Compare commits

...

8 Commits

Author SHA1 Message Date
Charlie Marsh
a310aed128 Bump version to 0.0.116 2022-11-13 13:46:05 -05:00
Harutaka Kawamura
43cc8bc84e Lint test code (#721) 2022-11-13 11:55:57 -05:00
Harutaka Kawamura
84bf36194b Implement B012 (#718) 2022-11-13 11:55:33 -05:00
Harutaka Kawamura
e4d168bb4f Implement B021 (#719) 2022-11-13 11:40:24 -05:00
Charlie Marsh
439642addf Regenerate README 2022-11-13 10:55:14 -05:00
Charlie Marsh
f5b1f957e3 Improve some import tracking code (#715) 2022-11-13 00:10:13 -05:00
Charlie Marsh
8f99705795 Make some error messages more grammatically consistent 2022-11-12 16:57:23 -05:00
Charlie Marsh
9ec7e6bcd6 Add function name to B008 message 2022-11-12 16:53:13 -05:00
35 changed files with 810 additions and 247 deletions

View File

@@ -77,7 +77,7 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- run: cargo clippy --all -- -D warnings
- run: cargo clippy --all-targets --all-features -- -D warnings
cargo_test:
name: "cargo test"

View File

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

6
Cargo.lock generated
View File

@@ -930,7 +930,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.115-dev.0"
version = "0.0.116-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -2237,7 +2237,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.115"
version = "0.0.116"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2285,7 +2285,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.115"
version = "0.0.116"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

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

View File

@@ -99,7 +99,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.115
rev: v0.0.116
hooks:
- id: ruff
```
@@ -335,7 +335,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F702 | ContinueOutsideLoop | `continue` not properly in loop | |
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | |
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | |
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | |
| F707 | DefaultExceptNotLast | An `except` block as not the last exception handler | |
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | |
| F821 | UndefinedName | Undefined name `...` | |
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
@@ -409,7 +409,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
| D400 | EndsInPeriod | First line should end with a period | |
| D402 | NoSignature | First line should not be the function's signature | |
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
| D404 | NoThisPrefix | First word of the docstring should not be 'This' | |
| D404 | NoThisPrefix | First word of the docstring should not be "This" | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 |
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 |
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 |
@@ -440,7 +440,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 | 🛠 |
| U009 | PEP3120UnnecessaryCodingComment | UTF-8 encoding declaration is unnecessary | 🛠 |
| U010 | UnnecessaryFutureImport | Unnecessary `__future__` import `...` for target Python version | 🛠 |
| U011 | UnnecessaryLRUCacheParams | Unnecessary parameters to `functools.lru_cache` | 🛠 |
| U012 | UnnecessaryEncodeUTF8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
@@ -476,9 +476,9 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
| S101 | AssertUsed | Use of `assert` detected | |
| S102 | ExecUsed | Use of `exec` detected | |
| S104 | HardcodedBindAllInterfaces | Possible binding to all interfaces | |
| S105 | HardcodedPasswordString | Possible hardcoded password: `'...'` | |
| S106 | HardcodedPasswordFuncArg | Possible hardcoded password: `'...'` | |
| S107 | HardcodedPasswordDefault | Possible hardcoded password: `'...'` | |
| S105 | HardcodedPasswordString | Possible hardcoded password: `"..."` | |
| S106 | HardcodedPasswordFuncArg | Possible hardcoded password: `"..."` | |
| S107 | HardcodedPasswordDefault | Possible hardcoded password: `"..."` | |
### flake8-comprehensions
@@ -509,25 +509,27 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment. | |
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment | |
| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment | |
| B004 | UnreliableCallableCheck | Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results. | |
| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader. | |
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | |
| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader | |
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function call in argument defaults | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
| B012 | JumpStatementInFinally | `return/continue/break` inside finally blocks cause exceptions to be silenced | |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError` instead of `except (ValueError,)`. | |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
| B015 | UselessComparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | |
| B016 | CannotRaiseLiteral | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
| B017 | NoAssertRaisesException | `assertRaises(Exception)` should be considered evil | |
| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks. | |
| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged. | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
### flake8-builtins
@@ -700,7 +702,7 @@ including:
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (22/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (15/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
@@ -725,7 +727,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (22/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),

View File

@@ -1,11 +1,11 @@
use std::fs;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ropey::Rope;
use ruff::fs;
fn criterion_benchmark(c: &mut Criterion) {
let contents = fs::read_file(Path::new("resources/test/fixtures/D.py")).unwrap();
let contents = fs::read_to_string(Path::new("resources/test/fixtures/D.py")).unwrap();
c.bench_function("rope", |b| {
b.iter(|| {
let rope = Rope::from_str(black_box(&contents));

View File

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

View File

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

107
resources/test/fixtures/B012.py vendored Normal file
View File

@@ -0,0 +1,107 @@
def a():
try:
pass
finally:
return # warning
def b():
try:
pass
finally:
if 1 + 0 == 2 - 1:
return # warning
def c():
try:
pass
finally:
try:
return # warning
except Exception:
pass
def d():
try:
try:
pass
finally:
return # warning
finally:
pass
def e():
if 1 == 2 - 1:
try:
def f():
try:
pass
finally:
return # warning
finally:
pass
def g():
try:
pass
finally:
def h():
return # no warning
e()
def i():
while True:
try:
pass
finally:
break # warning
def j():
while True:
break # no warning
def h():
while True:
try:
pass
finally:
continue # warning
def j():
while True:
continue # no warning
def k():
try:
pass
finally:
while True:
break # no warning
while True:
continue # no warning
while True:
return # warning
while True:
try:
pass
finally:
continue # warning
while True:
try:
pass
finally:
break # warning

76
resources/test/fixtures/B021.py vendored Normal file
View File

@@ -0,0 +1,76 @@
f"""
Should emit:
B021 - on lines 14, 22, 30, 38, 46, 54, 62, 70, 73
"""
VARIABLE = "world"
def foo1():
"""hello world!"""
def foo2():
f"""hello {VARIABLE}!"""
class bar1:
"""hello world!"""
class bar2:
f"""hello {VARIABLE}!"""
def foo1():
"""hello world!"""
def foo2():
f"""hello {VARIABLE}!"""
class bar1:
"""hello world!"""
class bar2:
f"""hello {VARIABLE}!"""
def foo1():
"hello world!"
def foo2():
f"hello {VARIABLE}!"
class bar1:
"hello world!"
class bar2:
f"hello {VARIABLE}!"
def foo1():
"hello world!"
def foo2():
f"hello {VARIABLE}!"
class bar1:
"hello world!"
class bar2:
f"hello {VARIABLE}!"
def baz():
f"""I'm probably a docstring: {VARIABLE}!"""
print(f"""I'm a normal string""")
f"""Don't detect me!"""

View File

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

View File

@@ -1,4 +1,4 @@
use fnv::FnvHashSet;
use fnv::{FnvHashMap, FnvHashSet};
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
@@ -19,6 +19,7 @@ fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
}
}
/// Convert an `Expr` to its call path (like `List`, or `typing.List`).
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
@@ -38,31 +39,6 @@ pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
}
}
/// Return `true` if the `Expr` is a reference to `${module}.${target}`.
///
/// Useful for, e.g., ensuring that a `Union` reference represents
/// `typing.Union`.
pub fn match_name_or_attr_from_module(
expr: &Expr,
target: &str,
module: &str,
imports: Option<&FnvHashSet<&str>>,
) -> bool {
match &expr.node {
ExprKind::Attribute { value, attr, .. } => match &value.node {
ExprKind::Name { id, .. } => id == module && target == attr,
_ => false,
},
ExprKind::Name { id, .. } => {
target == id
&& imports
.map(|imports| imports.contains(&id.as_str()))
.unwrap_or_default()
}
_ => false,
}
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
@@ -140,3 +116,145 @@ pub fn to_absolute(relative: &Location, base: &Location) -> Location {
Location::new(relative.row() + base.row() - 1, relative.column())
}
}
/// Return `true` if the `Expr` is a reference to `${module}.${target}`.
///
/// Useful for, e.g., ensuring that a `Union` reference represents
/// `typing.Union`.
pub fn match_module_member(
expr: &Expr,
target: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
compose_call_path(expr)
.map(|expr| match_call_path(&expr, target, from_imports))
.unwrap_or(false)
}
/// Return `true` if the `call_path` is a reference to `${module}.${target}`.
///
/// Optimized version of `match_module_member` for pre-computed call paths.
pub fn match_call_path(
call_path: &str,
target: &str,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
// Case (1a): it's the same call path (`import typing`, `typing.re.Match`).
// Case (1b): it's the same call path (`import typing.re`, `typing.re.Match`).
if call_path == target {
return true;
}
if let Some((parent, member)) = target.rsplit_once('.') {
// Case (2): We imported star from the parent (`from typing.re import *`,
// `Match`).
if call_path == member
&& from_imports
.get(parent)
.map(|imports| imports.contains("*"))
.unwrap_or(false)
{
return true;
}
// Case (3): We imported from the parent (`from typing.re import Match`,
// `Match`)
if call_path == member
&& from_imports
.get(parent)
.map(|imports| imports.contains(member))
.unwrap_or(false)
{
return true;
}
}
// Case (4): We imported from the grandparent (`from typing import re`,
// `re.Match`)
let mut parts = target.rsplitn(3, '.');
let member = parts.next();
let parent = parts.next();
let grandparent = parts.next();
if let (Some(member), Some(parent), Some(grandparent)) = (member, parent, grandparent) {
if call_path == format!("{parent}.{member}")
&& from_imports
.get(grandparent)
.map(|imports| imports.contains(parent))
.unwrap_or(false)
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_parser::parser;
use crate::ast::helpers::match_module_member;
#[test]
fn fully_qualified() -> Result<()> {
let expr = parser::parse_expression("typing.re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::default()
));
Ok(())
}
#[test]
fn unimported() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::default(),
));
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(!match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::default(),
));
Ok(())
}
#[test]
fn from_star() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["*"]))])
));
Ok(())
}
#[test]
fn from_parent() -> Result<()> {
let expr = parser::parse_expression("Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing.re", FnvHashSet::from_iter(["Match"]))])
));
Ok(())
}
#[test]
fn from_grandparent() -> Result<()> {
let expr = parser::parse_expression("re.Match", "<filename>")?;
assert!(match_module_member(
&expr,
"typing.re.Match",
&FnvHashMap::from_iter([("typing", FnvHashSet::from_iter(["re"]))])
));
Ok(())
}
}

View File

@@ -67,9 +67,8 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
}
/// Check if a node is parent of a conditional branch.
pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
pub fn on_conditional_branch<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
parents.any(|parent| {
if matches!(parent.node, StmtKind::If { .. } | StmtKind::While { .. }) {
return true;
}
@@ -78,24 +77,18 @@ pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool
return true;
}
}
}
false
false
})
}
/// Check if a node is in a nested block.
pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
for index in parent_stack.iter().rev() {
let parent = parents[*index];
if matches!(
pub fn in_nested_block<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
parents.any(|parent| {
matches!(
parent.node,
StmtKind::Try { .. } | StmtKind::If { .. } | StmtKind::With { .. }
) {
return true;
}
}
false
)
})
}
/// Check if a node represents an unpacking assignment.

View File

@@ -12,7 +12,7 @@ use rustpython_parser::ast::{
};
use rustpython_parser::parser;
use crate::ast::helpers::{extract_handler_names, match_name_or_attr_from_module};
use crate::ast::helpers::{extract_handler_names, match_module_member};
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
@@ -155,13 +155,12 @@ impl<'a> Checker<'a> {
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
pub fn match_typing_module(&self, expr: &Expr, target: &str) -> bool {
match_name_or_attr_from_module(expr, target, "typing", self.from_imports.get("typing"))
match_module_member(expr, &format!("typing.{target}"), &self.from_imports)
|| (typing::in_extensions(target)
&& match_name_or_attr_from_module(
&& match_module_member(
expr,
target,
"typing_extensions",
self.from_imports.get("typing_extensions"),
&format!("typing_extensions.{target}"),
&self.from_imports,
))
}
}
@@ -192,7 +191,13 @@ where
self.futures_allowed = false;
if !self.seen_import_boundary
&& !helpers::is_assignment_to_a_dunder(node)
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
&& !operations::in_nested_block(
&mut self
.parent_stack
.iter()
.rev()
.map(|index| self.parents[*index]),
)
{
self.seen_import_boundary = true;
}
@@ -870,6 +875,9 @@ where
let prev_visible_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
@@ -889,6 +897,9 @@ where
));
}
StmtKind::ClassDef { body, .. } => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
let definition = docstrings::extraction::extract(
&self.visible_scope,
stmt,
@@ -911,6 +922,9 @@ where
finalbody,
} => {
self.except_handlers.push(extract_handler_names(handlers));
if self.settings.enabled.contains(&CheckCode::B012) {
flake8_bugbear::plugins::jump_statement_in_finally(self, finalbody);
}
for stmt in body {
self.visit_stmt(stmt);
}
@@ -1019,7 +1033,7 @@ where
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
&& typing::is_pep585_builtin(expr, &self.from_imports)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
@@ -1051,7 +1065,7 @@ where
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
&& typing::is_pep585_builtin(expr, &self.from_imports)
{
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
@@ -2214,7 +2228,13 @@ impl<'a> Checker<'a> {
fn handle_node_delete(&mut self, expr: &Expr) {
if let ExprKind::Name { id, .. } = &expr.node {
if operations::on_conditional_branch(&self.parent_stack, &self.parents) {
if operations::on_conditional_branch(
&mut self
.parent_stack
.iter()
.rev()
.map(|index| self.parents[*index]),
) {
return;
}
@@ -2234,6 +2254,9 @@ impl<'a> Checker<'a> {
where
'b: 'a,
{
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, python_ast);
}
let docstring = docstrings::extraction::docstring_from(python_ast);
self.definitions.push((
Definition {

View File

@@ -283,7 +283,7 @@ mod tests {
},
&fixer::Mode::Generate,
);
return checks;
checks
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());

View File

@@ -87,6 +87,7 @@ pub enum CheckCode {
B009,
B010,
B011,
B012,
B013,
B014,
B015,
@@ -94,6 +95,7 @@ pub enum CheckCode {
B017,
B018,
B019,
B021,
B025,
B026,
// flake8-comprehensions
@@ -382,10 +384,11 @@ pub enum CheckKind {
StripWithMultiCharacters,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault,
FunctionCallArgumentDefault(Option<String>),
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
JumpStatementInFinally(String),
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
UselessComparison,
@@ -393,6 +396,7 @@ pub enum CheckKind {
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
FStringDocstring,
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
// flake8-comprehensions
@@ -619,10 +623,13 @@ impl CheckCode {
CheckCode::B005 => CheckKind::StripWithMultiCharacters,
CheckCode::B006 => CheckKind::MutableArgumentDefault,
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault,
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault(None),
CheckCode::B009 => CheckKind::GetAttrWithConstant,
CheckCode::B010 => CheckKind::SetAttrWithConstant,
CheckCode::B011 => CheckKind::DoNotAssertFalse,
CheckCode::B012 => {
CheckKind::JumpStatementInFinally("return/continue/break".to_string())
}
CheckCode::B013 => {
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
}
@@ -632,6 +639,7 @@ impl CheckCode {
CheckCode::B017 => CheckKind::NoAssertRaisesException,
CheckCode::B018 => CheckKind::UselessExpression,
CheckCode::B019 => CheckKind::CachedInstanceMethod,
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
// flake8-comprehensions
@@ -865,6 +873,7 @@ impl CheckCode {
CheckCode::B009 => CheckCategory::Flake8Bugbear,
CheckCode::B010 => CheckCategory::Flake8Bugbear,
CheckCode::B011 => CheckCategory::Flake8Bugbear,
CheckCode::B012 => CheckCategory::Flake8Bugbear,
CheckCode::B013 => CheckCategory::Flake8Bugbear,
CheckCode::B014 => CheckCategory::Flake8Bugbear,
CheckCode::B015 => CheckCategory::Flake8Bugbear,
@@ -872,6 +881,7 @@ impl CheckCode {
CheckCode::B017 => CheckCategory::Flake8Bugbear,
CheckCode::B018 => CheckCategory::Flake8Bugbear,
CheckCode::B019 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
@@ -1064,10 +1074,11 @@ impl CheckKind {
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::FunctionCallArgumentDefault => &CheckCode::B008,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::UselessComparison => &CheckCode::B015,
@@ -1075,6 +1086,7 @@ impl CheckKind {
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
// flake8-comprehensions
@@ -1236,7 +1248,7 @@ impl CheckKind {
CheckKind::BreakOutsideLoop => "`break` outside loop".to_string(),
CheckKind::ContinueOutsideLoop => "`continue` not properly in loop".to_string(),
CheckKind::DefaultExceptNotLast => {
"An `except:` block as not the last exception handler".to_string()
"An `except` block as not the last exception handler".to_string()
}
CheckKind::DoNotAssignLambda => {
"Do not assign a lambda expression, use a def".to_string()
@@ -1373,40 +1385,47 @@ impl CheckKind {
`+(+(n))`, which equals `n`. You meant `n += 1`."
.to_string(),
CheckKind::AssignmentToOsEnviron => {
"Assigning to `os.environ` doesn't clear the environment.".to_string()
"Assigning to `os.environ` doesn't clear the environment".to_string()
}
CheckKind::UnreliableCallableCheck => " Using `hasattr(x, '__call__')` to test if x \
is callable is unreliable. Use `callable(x)` \
for consistent results."
.to_string(),
CheckKind::StripWithMultiCharacters => "Using `.strip()` with multi-character strings \
is misleading the reader."
.to_string(),
CheckKind::StripWithMultiCharacters => {
"Using `.strip()` with multi-character strings is misleading the reader".to_string()
}
CheckKind::MutableArgumentDefault => {
"Do not use mutable data structures for argument defaults.".to_string()
"Do not use mutable data structures for argument defaults".to_string()
}
CheckKind::UnusedLoopControlVariable(name) => format!(
"Loop control variable `{name}` not used within the loop body. If this is \
intended, start the name with an underscore."
),
CheckKind::FunctionCallArgumentDefault => {
"Do not perform function calls in argument defaults.".to_string()
CheckKind::FunctionCallArgumentDefault(name) => {
if let Some(name) = name {
format!("Do not perform function call `{name}` in argument defaults")
} else {
"Do not perform function call in argument defaults".to_string()
}
}
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
value, it is not any safer than normal property \
value. It is not any safer than normal property \
access."
.to_string(),
CheckKind::SetAttrWithConstant => "Do not call `setattr` with a constant attribute \
value, it is not any safer than normal property \
value. It is not any safer than normal property \
access."
.to_string(),
CheckKind::DoNotAssertFalse => "Do not `assert False` (`python -O` removes these \
calls), raise `AssertionError()`"
.to_string(),
CheckKind::JumpStatementInFinally(name) => {
format!("`{name}` inside finally blocks cause exceptions to be silenced")
}
CheckKind::RedundantTupleInExceptionHandler(name) => {
format!(
"A length-one tuple literal is redundant. Write `except {name}:` instead of \
`except ({name},):`."
"A length-one tuple literal is redundant. Write `except {name}` instead of \
`except ({name},)`."
)
}
CheckKind::UselessComparison => "Pointless comparison. This comparison does nothing \
@@ -1426,7 +1445,7 @@ impl CheckKind {
}
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil. It can lead to your test \
"`assertRaises(Exception)` should be considered evil. It can lead to your test \
passing even if the code being tested is never executed due to a typo. Either \
assert for a more specific exception (builtin or custom), use \
`assertRaisesRegex`, or use the context manager form of `assertRaises`."
@@ -1436,16 +1455,18 @@ impl CheckKind {
"Found useless expression. Either assign it to a variable or remove it.".to_string()
}
CheckKind::CachedInstanceMethod => "Use of `functools.lru_cache` or `functools.cache` \
on methods can lead to memory leaks."
on methods can lead to memory leaks"
.to_string(),
CheckKind::FStringDocstring => "f-string used as docstring. This will be interpreted \
by python as a joined string rather than a docstring."
.to_string(),
CheckKind::DuplicateTryBlockException(name) => {
format!("try-except block with duplicate exception `{name}`")
}
CheckKind::StarArgUnpackingAfterKeywordArg => {
"Star-arg unpacking after a keyword argument is strongly discouraged, because it \
only works when the keyword parameter is declared after all parameters supplied \
by the unpacked sequence, and this change of ordering can surprise and mislead \
readers."
"Star-arg unpacking after a keyword argument is strongly discouraged. It only \
works when the keyword parameter is declared after all parameters supplied by the \
unpacked sequence, and this change of ordering can surprise and mislead readers."
.to_string()
}
// flake8-comprehensions
@@ -1703,7 +1724,7 @@ impl CheckKind {
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
CheckKind::PublicInit => "Missing docstring in `__init__`".to_string(),
CheckKind::NoThisPrefix => {
"First word of the docstring should not be 'This'".to_string()
"First word of the docstring should not be \"This\"".to_string()
}
CheckKind::SkipDocstring => {
"Function decorated with `@overload` shouldn't contain a docstring".to_string()
@@ -1811,7 +1832,7 @@ impl CheckKind {
format!("Exception name `{name}` should be named with an Error suffix")
}
CheckKind::PEP3120UnnecessaryCodingComment => {
"utf-8 encoding declaration is unnecessary".to_string()
"UTF-8 encoding declaration is unnecessary".to_string()
}
// isort
CheckKind::UnsortedImports => "Import block is un-sorted or un-formatted".to_string(),
@@ -1822,13 +1843,13 @@ impl CheckKind {
"Possible binding to all interfaces".to_string()
}
CheckKind::HardcodedPasswordString(string) => {
format!("Possible hardcoded password: `'{string}'`")
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordFuncArg(string) => {
format!("Possible hardcoded password: `'{string}'`")
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordDefault(string) => {
format!("Possible hardcoded password: `'{string}'`")
format!("Possible hardcoded password: `\"{string}\"`")
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
@@ -1874,16 +1895,16 @@ impl CheckKind {
pub fn summary(&self) -> String {
match self {
CheckKind::UnaryPrefixIncrement => {
"Python does not support the unary prefix increment.".to_string()
"Python does not support the unary prefix increment".to_string()
}
CheckKind::UnusedLoopControlVariable(name) => {
format!("Loop control variable `{name}` not used within the loop body.")
format!("Loop control variable `{name}` not used within the loop body")
}
CheckKind::NoAssertRaisesException => {
"`assertRaises(Exception):` should be considered evil.".to_string()
"`assertRaises(Exception)` should be considered evil".to_string()
}
CheckKind::StarArgUnpackingAfterKeywordArg => {
"Star-arg unpacking after a keyword argument is strongly discouraged.".to_string()
"Star-arg unpacking after a keyword argument is strongly discouraged".to_string()
}
_ => self.body(),
}

View File

@@ -47,6 +47,7 @@ pub enum CheckCodePrefix {
B01,
B010,
B011,
B012,
B013,
B014,
B015,
@@ -55,6 +56,7 @@ pub enum CheckCodePrefix {
B018,
B019,
B02,
B021,
B025,
B026,
C,
@@ -373,6 +375,7 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -380,6 +383,7 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B021,
CheckCode::B025,
CheckCode::B026,
],
@@ -394,6 +398,7 @@ impl CheckCodePrefix {
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -401,6 +406,7 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B021,
CheckCode::B025,
CheckCode::B026,
],
@@ -425,6 +431,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B01 => vec![
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
@@ -435,6 +442,7 @@ impl CheckCodePrefix {
],
CheckCodePrefix::B010 => vec![CheckCode::B010],
CheckCodePrefix::B011 => vec![CheckCode::B011],
CheckCodePrefix::B012 => vec![CheckCode::B012],
CheckCodePrefix::B013 => vec![CheckCode::B013],
CheckCodePrefix::B014 => vec![CheckCode::B014],
CheckCodePrefix::B015 => vec![CheckCode::B015],
@@ -442,7 +450,8 @@ impl CheckCodePrefix {
CheckCodePrefix::B017 => vec![CheckCode::B017],
CheckCodePrefix::B018 => vec![CheckCode::B018],
CheckCodePrefix::B019 => vec![CheckCode::B019],
CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026],
CheckCodePrefix::B02 => vec![CheckCode::B021, CheckCode::B025, CheckCode::B026],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::C => vec![
@@ -1176,6 +1185,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B01 => PrefixSpecificity::Tens,
CheckCodePrefix::B010 => PrefixSpecificity::Explicit,
CheckCodePrefix::B011 => PrefixSpecificity::Explicit,
CheckCodePrefix::B012 => PrefixSpecificity::Explicit,
CheckCodePrefix::B013 => PrefixSpecificity::Explicit,
CheckCodePrefix::B014 => PrefixSpecificity::Explicit,
CheckCodePrefix::B015 => PrefixSpecificity::Explicit,
@@ -1184,6 +1194,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B018 => PrefixSpecificity::Explicit,
CheckCodePrefix::B019 => PrefixSpecificity::Explicit,
CheckCodePrefix::B02 => PrefixSpecificity::Tens,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::C => PrefixSpecificity::Category,
@@ -1338,15 +1349,6 @@ impl CheckCodePrefix {
CheckCodePrefix::I0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::I00 => PrefixSpecificity::Tens,
CheckCodePrefix::I001 => PrefixSpecificity::Explicit,
CheckCodePrefix::S => PrefixSpecificity::Category,
CheckCodePrefix::S1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::S10 => PrefixSpecificity::Tens,
CheckCodePrefix::S101 => PrefixSpecificity::Explicit,
CheckCodePrefix::S102 => PrefixSpecificity::Explicit,
CheckCodePrefix::S104 => PrefixSpecificity::Explicit,
CheckCodePrefix::S105 => PrefixSpecificity::Explicit,
CheckCodePrefix::S106 => PrefixSpecificity::Explicit,
CheckCodePrefix::S107 => PrefixSpecificity::Explicit,
CheckCodePrefix::M => PrefixSpecificity::Category,
CheckCodePrefix::M0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::M00 => PrefixSpecificity::Tens,
@@ -1383,6 +1385,15 @@ impl CheckCodePrefix {
CheckCodePrefix::RUF001 => PrefixSpecificity::Explicit,
CheckCodePrefix::RUF002 => PrefixSpecificity::Explicit,
CheckCodePrefix::RUF003 => PrefixSpecificity::Explicit,
CheckCodePrefix::S => PrefixSpecificity::Category,
CheckCodePrefix::S1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::S10 => PrefixSpecificity::Tens,
CheckCodePrefix::S101 => PrefixSpecificity::Explicit,
CheckCodePrefix::S102 => PrefixSpecificity::Explicit,
CheckCodePrefix::S104 => PrefixSpecificity::Explicit,
CheckCodePrefix::S105 => PrefixSpecificity::Explicit,
CheckCodePrefix::S106 => PrefixSpecificity::Explicit,
CheckCodePrefix::S107 => PrefixSpecificity::Explicit,
CheckCodePrefix::T => PrefixSpecificity::Category,
CheckCodePrefix::T2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::T20 => PrefixSpecificity::Tens,

View File

@@ -1,13 +1,13 @@
use num_bigint::BigInt;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
use crate::ast::helpers::match_name_or_attr_from_module;
use crate::ast::helpers::match_module_member;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
match_name_or_attr_from_module(expr, target, "sys", checker.from_imports.get("sys"))
match_module_member(expr, &format!("sys.{target}"), &checker.from_imports)
}
/// YTT101, YTT102, YTT301, YTT303
@@ -181,9 +181,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
/// YTT202
pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if match_name_or_attr_from_module(expr, "PY3", "six", checker.from_imports.get("six"))
&& checker.settings.enabled.contains(&CheckCode::YTT202)
{
if match_module_member(expr, "six.PY3", &checker.from_imports) {
checker.add_check(Check::new(
CheckKind::SixPY3Referenced,
Range::from_located(expr),

View File

@@ -1,22 +1,13 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::{compose_call_path, match_name_or_attr_from_module};
use crate::ast::helpers::{compose_call_path, match_module_member};
use crate::ast::types::{Range, ScopeKind};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
match_name_or_attr_from_module(
expr,
"lru_cache",
"functools",
checker.from_imports.get("functools"),
) || match_name_or_attr_from_module(
expr,
"cache",
"functools",
checker.from_imports.get("functools"),
)
match_module_member(expr, "functools.lru_cache", &checker.from_imports)
|| match_module_member(expr, "functools.cache", &checker.from_imports)
}
/// B019

View File

@@ -0,0 +1,19 @@
use rustpython_ast::{ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B021
pub fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
if let Some(stmt) = body.first() {
if let StmtKind::Expr { value } = &stmt.node {
if let ExprKind::JoinedStr { .. } = value.node {
checker.add_check(Check::new(
CheckKind::FStringDocstring,
Range::from_located(stmt),
));
}
}
}
}

View File

@@ -1,7 +1,7 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use crate::ast::helpers::compose_call_path;
use crate::ast::helpers::{compose_call_path, match_call_path};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
@@ -24,37 +24,14 @@ fn is_immutable_func(
extend_immutable_calls: &[&str],
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> bool {
compose_call_path(expr).map_or_else(
|| false,
|call_path| {
// It matches the call path exactly (`operator.methodcaller`).
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
if &call_path == target {
return true;
}
}
// It matches the member name, and was imported from that module (`methodcaller`
// following `from operator import methodcaller`).
if !call_path.contains('.') {
for target in IMMUTABLE_FUNCS.iter().chain(extend_immutable_calls) {
let mut splitter = target.rsplit('.');
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
if call_path == member
&& from_imports
.get(module)
.map(|module| module.contains(member))
.unwrap_or(false)
{
return true;
}
}
}
}
false
},
)
compose_call_path(expr)
.map(|call_path| {
IMMUTABLE_FUNCS
.iter()
.chain(extend_immutable_calls)
.any(|target| match_call_path(&call_path, target, from_imports))
})
.unwrap_or(false)
}
struct ArgumentDefaultVisitor<'a> {
@@ -75,7 +52,7 @@ where
&& !is_nan_or_infinity(func, args)
{
self.checks.push((
CheckKind::FunctionCallArgumentDefault,
CheckKind::FunctionCallArgumentDefault(compose_call_path(expr)),
Range::from_located(expr),
))
}

View File

@@ -0,0 +1,49 @@
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
for stmt in body {
if f(stmt) {
checker.add_check(Check::new(
CheckKind::JumpStatementInFinally(match &stmt.node {
StmtKind::Break { .. } => "break".to_string(),
StmtKind::Continue { .. } => "continue".to_string(),
StmtKind::Return { .. } => "return".to_string(),
_ => unreachable!(
"Expected StmtKind::Break | StmtKind::Continue | StmtKind::Return"
),
}),
Range::from_located(stmt),
));
}
match &stmt.node {
StmtKind::While { body, .. }
| StmtKind::For { body, .. }
| StmtKind::AsyncFor { body, .. } => {
walk_stmt(checker, body, |stmt| {
matches!(stmt.node, StmtKind::Return { .. })
});
}
StmtKind::If { body, .. }
| StmtKind::Try { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. } => {
walk_stmt(checker, body, f);
}
_ => {}
}
}
}
/// B012
pub fn jump_statement_in_finally(checker: &mut Checker, finalbody: &[Stmt]) {
walk_stmt(checker, finalbody, |stmt| {
matches!(
stmt.node,
StmtKind::Break | StmtKind::Continue | StmtKind::Return { .. }
)
});
}

View File

@@ -4,8 +4,10 @@ pub use assignment_to_os_environ::assignment_to_os_environ;
pub use cached_instance_method::cached_instance_method;
pub use cannot_raise_literal::cannot_raise_literal;
pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions};
pub use f_string_docstring::f_string_docstring;
pub use function_call_argument_default::function_call_argument_default;
pub use getattr_with_constant::getattr_with_constant;
pub use jump_statement_in_finally::jump_statement_in_finally;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
@@ -23,8 +25,10 @@ mod assignment_to_os_environ;
mod cached_instance_method;
mod cannot_raise_literal;
mod duplicate_exceptions;
mod f_string_docstring;
mod function_call_argument_default;
mod getattr_with_constant;
mod jump_statement_in_finally;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;

View File

@@ -1,7 +1,7 @@
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind};
use crate::ast::helpers::compose_call_path;
use crate::ast::helpers::{compose_call_path, match_call_path};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
@@ -17,37 +17,13 @@ const MUTABLE_FUNCS: [&str; 7] = [
];
pub fn is_mutable_func(expr: &Expr, from_imports: &FnvHashMap<&str, FnvHashSet<&str>>) -> bool {
compose_call_path(expr).map_or_else(
|| false,
|call_path| {
// It matches the call path exactly (`collections.Counter`).
for target in MUTABLE_FUNCS {
if call_path == target {
return true;
}
}
// It matches the member name, and was imported from that module (`Counter`
// following `from collections import Counter`).
if !call_path.contains('.') {
for target in MUTABLE_FUNCS {
let mut splitter = target.rsplit('.');
if let (Some(member), Some(module)) = (splitter.next(), splitter.next()) {
if call_path == member
&& from_imports
.get(module)
.map(|module| module.contains(member))
.unwrap_or(false)
{
return true;
}
}
}
}
false
},
)
compose_call_path(expr)
.map(|call_path| {
MUTABLE_FUNCS
.iter()
.any(|target| match_call_path(&call_path, target, from_imports))
})
.unwrap_or(false)
}
/// B006

View File

@@ -2,7 +2,8 @@
source: src/flake8_bugbear/mod.rs
expression: checks
---
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: Depends
location:
row: 19
column: 50

View File

@@ -336,6 +336,7 @@ mod tests {
#[test_case(CheckCode::B009, Path::new("B009_B010.py"); "B009")]
#[test_case(CheckCode::B010, Path::new("B009_B010.py"); "B010")]
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
#[test_case(CheckCode::B012, Path::new("B012.py"); "B012")]
#[test_case(CheckCode::B013, Path::new("B013.py"); "B013")]
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
#[test_case(CheckCode::B015, Path::new("B015.py"); "B015")]
@@ -343,6 +344,7 @@ mod tests {
#[test_case(CheckCode::B017, Path::new("B017.py"); "B017")]
#[test_case(CheckCode::B018, Path::new("B018.py"); "B018")]
#[test_case(CheckCode::B019, Path::new("B019.py"); "B019")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
@@ -515,7 +517,7 @@ mod tests {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures").join(path).as_path(),
&settings::Settings::for_rule(check_code.clone()),
&settings::Settings::for_rule(check_code),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);

View File

@@ -83,7 +83,7 @@ mod tests {
use crate::pep8_naming::helpers::{is_acronym, is_camelcase, is_mixed_case};
#[test]
fn test_is_camelcase() -> () {
fn test_is_camelcase() {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
@@ -93,7 +93,7 @@ mod tests {
}
#[test]
fn test_is_mixed_case() -> () {
fn test_is_mixed_case() {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
@@ -104,7 +104,7 @@ mod tests {
}
#[test]
fn test_is_acronym() -> () {
fn test_is_acronym() {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));

View File

@@ -27,7 +27,7 @@ mod tests {
use crate::python::string::{is_lower, is_upper};
#[test]
fn test_is_lower() -> () {
fn test_is_lower() {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
@@ -38,7 +38,7 @@ mod tests {
}
#[test]
fn test_is_upper() -> () {
fn test_is_upper() {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));

View File

@@ -209,7 +209,7 @@ pub enum SubscriptKind {
pub fn match_annotated_subscript(
expr: &Expr,
imports: &FnvHashMap<&str, FnvHashSet<&str>>,
from_imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> Option<SubscriptKind> {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
@@ -238,9 +238,9 @@ pub fn match_annotated_subscript(
// Verify that, e.g., `Union` is a reference to `typing.Union`.
if let Some(modules) = IMPORTED_SUBSCRIPTS.get(&id.as_str()) {
for module in modules {
if imports
if from_imports
.get(module)
.map(|imports| imports.contains(&id.as_str()))
.map(|imports| imports.contains(&id.as_str()) || imports.contains("*"))
.unwrap_or_default()
{
return if is_pep593_annotated_subscript(id) {
@@ -260,7 +260,7 @@ pub fn match_annotated_subscript(
/// Returns `true` if `Expr` represents a reference to a typing object with a
/// PEP 585 built-in. Note that none of the PEP 585 built-ins are in
/// `typing_extensions`.
pub fn is_pep585_builtin(expr: &Expr, typing_imports: Option<&FnvHashSet<&str>>) -> bool {
pub fn is_pep585_builtin(expr: &Expr, from_imports: &FnvHashMap<&str, FnvHashSet<&str>>) -> bool {
match &expr.node {
ExprKind::Attribute { attr, value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
@@ -270,8 +270,9 @@ pub fn is_pep585_builtin(expr: &Expr, typing_imports: Option<&FnvHashSet<&str>>)
}
}
ExprKind::Name { id, .. } => {
typing_imports
.map(|imports| imports.contains(&id.as_str()))
from_imports
.get("typing")
.map(|imports| imports.contains(&id.as_str()) || imports.contains("*"))
.unwrap_or_default()
&& PEP_585_BUILTINS_ELIGIBLE.contains(&id.as_str())
}

View File

@@ -1,4 +1,4 @@
use fnv::FnvHashSet;
use fnv::{FnvHashMap, FnvHashSet};
use rustpython_ast::{Constant, KeywordData};
use rustpython_parser::ast::{ArgData, Expr, ExprKind, Stmt, StmtKind};
@@ -163,7 +163,7 @@ pub fn type_of_primitive(func: &Expr, args: &[Expr], location: Range) -> Option<
pub fn unnecessary_lru_cache_params(
decorator_list: &[Expr],
target_version: PythonVersion,
imports: Option<&FnvHashSet<&str>>,
imports: &FnvHashMap<&str, FnvHashSet<&str>>,
) -> Option<Check> {
for expr in decorator_list.iter() {
if let ExprKind::Call {
@@ -172,8 +172,7 @@ pub fn unnecessary_lru_cache_params(
keywords,
} = &expr.node
{
if args.is_empty()
&& helpers::match_name_or_attr_from_module(func, "lru_cache", "functools", imports)
if args.is_empty() && helpers::match_module_member(func, "functools.lru_cache", imports)
{
// Ex) `functools.lru_cache()`
if keywords.is_empty() {

View File

@@ -8,7 +8,7 @@ pub fn unnecessary_lru_cache_params(checker: &mut Checker, decorator_list: &[Exp
if let Some(mut check) = checks::unnecessary_lru_cache_params(
decorator_list,
checker.settings.target_version,
checker.from_imports.get("functools"),
&checker.from_imports,
) {
if checker.patch() {
if let Some(fix) =

View File

@@ -2,7 +2,8 @@
source: src/linter.rs
expression: checks
---
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: range
location:
row: 85
column: 60
@@ -10,7 +11,8 @@ expression: checks
row: 85
column: 68
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: range
location:
row: 89
column: 63
@@ -18,7 +20,8 @@ expression: checks
row: 89
column: 71
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: range
location:
row: 93
column: 59
@@ -26,7 +29,8 @@ expression: checks
row: 93
column: 67
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: time.time
location:
row: 109
column: 38
@@ -34,7 +38,8 @@ expression: checks
row: 109
column: 49
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: dt.datetime.now
location:
row: 113
column: 11
@@ -42,7 +47,8 @@ expression: checks
row: 113
column: 28
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: dt.timedelta
location:
row: 113
column: 31
@@ -50,7 +56,8 @@ expression: checks
row: 113
column: 51
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: ~
location:
row: 117
column: 29
@@ -58,7 +65,8 @@ expression: checks
row: 117
column: 44
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: float
location:
row: 155
column: 33
@@ -66,7 +74,8 @@ expression: checks
row: 155
column: 47
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: float
location:
row: 160
column: 29
@@ -74,7 +83,8 @@ expression: checks
row: 160
column: 37
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: float
location:
row: 164
column: 44
@@ -82,7 +92,8 @@ expression: checks
row: 164
column: 57
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: float
location:
row: 170
column: 20
@@ -90,7 +101,8 @@ expression: checks
row: 170
column: 28
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: dt.datetime.now
location:
row: 170
column: 30
@@ -98,7 +110,8 @@ expression: checks
row: 170
column: 47
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: map
location:
row: 176
column: 21
@@ -106,7 +119,8 @@ expression: checks
row: 176
column: 62
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: random.randint
location:
row: 181
column: 18
@@ -114,7 +128,8 @@ expression: checks
row: 181
column: 59
fix: ~
- kind: FunctionCallArgumentDefault
- kind:
FunctionCallArgumentDefault: dt.datetime.now
location:
row: 181
column: 36

View File

@@ -0,0 +1,95 @@
---
source: src/linter.rs
expression: checks
---
- kind:
JumpStatementInFinally: return
location:
row: 5
column: 8
end_location:
row: 5
column: 14
fix: ~
- kind:
JumpStatementInFinally: return
location:
row: 13
column: 12
end_location:
row: 13
column: 18
fix: ~
- kind:
JumpStatementInFinally: return
location:
row: 21
column: 12
end_location:
row: 21
column: 18
fix: ~
- kind:
JumpStatementInFinally: return
location:
row: 31
column: 12
end_location:
row: 31
column: 18
fix: ~
- kind:
JumpStatementInFinally: return
location:
row: 44
column: 20
end_location:
row: 44
column: 26
fix: ~
- kind:
JumpStatementInFinally: break
location:
row: 66
column: 12
end_location:
row: 66
column: 17
fix: ~
- kind:
JumpStatementInFinally: continue
location:
row: 78
column: 12
end_location:
row: 78
column: 20
fix: ~
- kind:
JumpStatementInFinally: return
location:
row: 94
column: 12
end_location:
row: 94
column: 18
fix: ~
- kind:
JumpStatementInFinally: continue
location:
row: 101
column: 8
end_location:
row: 101
column: 16
fix: ~
- kind:
JumpStatementInFinally: break
location:
row: 107
column: 8
end_location:
row: 107
column: 13
fix: ~

View File

@@ -0,0 +1,85 @@
---
source: src/linter.rs
expression: checks
---
- kind: FStringDocstring
location:
row: 1
column: 0
end_location:
row: 4
column: 3
fix: ~
- kind: FStringDocstring
location:
row: 14
column: 4
end_location:
row: 14
column: 28
fix: ~
- kind: FStringDocstring
location:
row: 22
column: 4
end_location:
row: 22
column: 28
fix: ~
- kind: FStringDocstring
location:
row: 30
column: 4
end_location:
row: 30
column: 28
fix: ~
- kind: FStringDocstring
location:
row: 38
column: 4
end_location:
row: 38
column: 28
fix: ~
- kind: FStringDocstring
location:
row: 46
column: 4
end_location:
row: 46
column: 24
fix: ~
- kind: FStringDocstring
location:
row: 54
column: 4
end_location:
row: 54
column: 24
fix: ~
- kind: FStringDocstring
location:
row: 62
column: 4
end_location:
row: 62
column: 24
fix: ~
- kind: FStringDocstring
location:
row: 70
column: 4
end_location:
row: 70
column: 24
fix: ~
- kind: FStringDocstring
location:
row: 74
column: 4
end_location:
row: 74
column: 48
fix: ~

View File

@@ -6,7 +6,7 @@ use assert_cmd::{crate_name, Command};
#[test]
fn test_stdin_success() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
cmd.args(&["-"]).write_stdin("").assert().success();
cmd.args(["-"]).write_stdin("").assert().success();
Ok(())
}
@@ -14,7 +14,7 @@ fn test_stdin_success() -> Result<()> {
fn test_stdin_error() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-"])
.args(["-"])
.write_stdin("import os\n")
.assert()
.failure();
@@ -26,7 +26,7 @@ fn test_stdin_error() -> Result<()> {
fn test_stdin_filename() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--stdin-filename", "F401.py"])
.args(["-", "--stdin-filename", "F401.py"])
.write_stdin("import os\n")
.assert()
.failure();
@@ -38,7 +38,7 @@ fn test_stdin_filename() -> Result<()> {
fn test_stdin_autofix() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--fix"])
.args(["-", "--fix"])
.write_stdin("import os\nimport sys\n\nprint(sys.version)\n")
.assert()
.success();
@@ -53,7 +53,7 @@ fn test_stdin_autofix() -> Result<()> {
fn test_stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--fix"])
.args(["-", "--fix"])
.write_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n")
.assert()
.failure();
@@ -68,7 +68,7 @@ fn test_stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<(
fn test_stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--fix"])
.args(["-", "--fix"])
.write_stdin("import sys\n\nprint(sys.version)\n")
.assert()
.success();