mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:37 +00:00
Improve handling of builtin symbols in linter rules (#10919)
Add a new method to the semantic model to simplify and improve the correctness of a common pattern
This commit is contained in:
parent
effd5188c9
commit
f779babc5f
93 changed files with 886 additions and 588 deletions
|
@ -6,6 +6,25 @@ def this_is_a_bug():
|
|||
print("Ooh, callable! Or is it?")
|
||||
|
||||
|
||||
def still_a_bug():
|
||||
import builtins
|
||||
o = object()
|
||||
if builtins.hasattr(o, "__call__"):
|
||||
print("B U G")
|
||||
if builtins.getattr(o, "__call__", False):
|
||||
print("B U G")
|
||||
|
||||
|
||||
def trickier_fix_for_this_one():
|
||||
o = object()
|
||||
|
||||
def callable(x):
|
||||
return True
|
||||
|
||||
if hasattr(o, "__call__"):
|
||||
print("STILL a bug!")
|
||||
|
||||
|
||||
def this_is_fine():
|
||||
o = object()
|
||||
if callable(o):
|
||||
|
|
|
@ -64,3 +64,6 @@ setattr(*foo, "bar", None)
|
|||
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739800901
|
||||
getattr(self.
|
||||
registration.registry, '__name__')
|
||||
|
||||
import builtins
|
||||
builtins.getattr(foo, "bar")
|
||||
|
|
|
@ -23,3 +23,7 @@ zip([1, 2, 3], repeat(1, times=None))
|
|||
# Errors (limited iterators).
|
||||
zip([1, 2, 3], repeat(1, 1))
|
||||
zip([1, 2, 3], repeat(1, times=4))
|
||||
|
||||
import builtins
|
||||
# Still an error even though it uses the qualified name
|
||||
builtins.zip([1, 2, 3])
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# PIE808
|
||||
range(0, 10)
|
||||
|
||||
import builtins
|
||||
builtins.range(0, 10)
|
||||
|
||||
# OK
|
||||
range(x, 10)
|
||||
range(-15, 10)
|
||||
|
|
|
@ -73,3 +73,10 @@ class BadFive:
|
|||
class BadSix:
|
||||
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
||||
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
|
||||
|
||||
|
||||
def isolated_scope():
|
||||
from builtins import type as Type
|
||||
|
||||
class ShouldNotError:
|
||||
def __exit__(self, typ: Type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
|
||||
|
|
|
@ -19,3 +19,9 @@ class Bad(Tuple[str, int, float]): # SLOT001
|
|||
|
||||
class Good(Tuple[str, int, float]): # OK
|
||||
__slots__ = ("foo",)
|
||||
|
||||
|
||||
import builtins
|
||||
|
||||
class AlsoBad(builtins.tuple[int, int]): # SLOT001
|
||||
pass
|
||||
|
|
|
@ -80,3 +80,8 @@ for i in list(foo_list): # OK
|
|||
for i in list(foo_list): # OK
|
||||
if True:
|
||||
del foo_list[i + 1]
|
||||
|
||||
import builtins
|
||||
|
||||
for i in builtins.list(nested_tuple): # PERF101
|
||||
pass
|
||||
|
|
|
@ -138,3 +138,8 @@ np.dtype(int) == float
|
|||
|
||||
#: E721
|
||||
dtype == float
|
||||
|
||||
import builtins
|
||||
|
||||
if builtins.type(res) == memoryview: # E721
|
||||
pass
|
||||
|
|
|
@ -4,3 +4,8 @@ def f() -> None:
|
|||
|
||||
def g() -> None:
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def h() -> None:
|
||||
NotImplementedError = "foo"
|
||||
raise NotImplemented
|
||||
|
|
|
@ -32,3 +32,6 @@ pathlib.Path(NAME).open(mode)
|
|||
pathlib.Path(NAME).open("rwx") # [bad-open-mode]
|
||||
pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
|
||||
pathlib.Path(NAME).open("rwx", encoding="utf-8") # [bad-open-mode]
|
||||
|
||||
import builtins
|
||||
builtins.open(NAME, "Ua", encoding="utf-8")
|
||||
|
|
|
@ -47,6 +47,12 @@ if y == np.nan:
|
|||
if y == npy_nan:
|
||||
pass
|
||||
|
||||
import builtins
|
||||
|
||||
# PLW0117
|
||||
if x == builtins.float("nan"):
|
||||
pass
|
||||
|
||||
# OK
|
||||
if math.isnan(x):
|
||||
pass
|
||||
|
|
|
@ -39,3 +39,6 @@ max(max(tuples_list))
|
|||
|
||||
# Starred argument should be copied as it is.
|
||||
max(1, max(*a))
|
||||
|
||||
import builtins
|
||||
builtins.min(1, min(2, 3))
|
||||
|
|
|
@ -152,3 +152,9 @@ object = A
|
|||
|
||||
class B(object):
|
||||
...
|
||||
|
||||
|
||||
import builtins
|
||||
|
||||
class Unusual(builtins.object):
|
||||
...
|
||||
|
|
|
@ -59,6 +59,21 @@ with open("file.txt", "w", newline="\r\n") as f:
|
|||
f.write(foobar)
|
||||
|
||||
|
||||
import builtins
|
||||
|
||||
|
||||
# FURB103
|
||||
with builtins.open("file.txt", "w", newline="\r\n") as f:
|
||||
f.write(foobar)
|
||||
|
||||
|
||||
from builtins import open as o
|
||||
|
||||
|
||||
# FURB103
|
||||
with o("file.txt", "w", newline="\r\n") as f:
|
||||
f.write(foobar)
|
||||
|
||||
# Non-errors.
|
||||
|
||||
with open("file.txt", errors="ignore", mode="wb") as f:
|
||||
|
|
|
@ -41,6 +41,22 @@ def func():
|
|||
pass
|
||||
|
||||
|
||||
import builtins
|
||||
|
||||
|
||||
with builtins.open("FURB129.py") as f:
|
||||
for line in f.readlines():
|
||||
pass
|
||||
|
||||
|
||||
from builtins import open as o
|
||||
|
||||
|
||||
with o("FURB129.py") as f:
|
||||
for line in f.readlines():
|
||||
pass
|
||||
|
||||
|
||||
# False positives
|
||||
def func(f):
|
||||
for _line in f.readlines():
|
||||
|
|
|
@ -12,6 +12,8 @@ dict.fromkeys(pierogi_fillings, {})
|
|||
dict.fromkeys(pierogi_fillings, set())
|
||||
dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
dict.fromkeys(pierogi_fillings, dict())
|
||||
import builtins
|
||||
builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
|
||||
# Okay.
|
||||
dict.fromkeys(pierogi_fillings)
|
||||
|
|
|
@ -784,17 +784,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
|||
}) => {
|
||||
let mut handled_exceptions = Exceptions::empty();
|
||||
for type_ in extract_handled_exceptions(handlers) {
|
||||
if let Some(qualified_name) = self.semantic.resolve_qualified_name(type_) {
|
||||
match qualified_name.segments() {
|
||||
["", "NameError"] => {
|
||||
handled_exceptions |= Exceptions::NAME_ERROR;
|
||||
}
|
||||
["", "ModuleNotFoundError"] => {
|
||||
if let Some(builtins_name) = self.semantic.resolve_builtin_symbol(type_) {
|
||||
match builtins_name {
|
||||
"NameError" => handled_exceptions |= Exceptions::NAME_ERROR,
|
||||
"ModuleNotFoundError" => {
|
||||
handled_exceptions |= Exceptions::MODULE_NOT_FOUND_ERROR;
|
||||
}
|
||||
["", "ImportError"] => {
|
||||
handled_exceptions |= Exceptions::IMPORT_ERROR;
|
||||
}
|
||||
"ImportError" => handled_exceptions |= Exceptions::IMPORT_ERROR,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1125,7 +1121,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
|||
]
|
||||
) {
|
||||
Some(typing::Callable::MypyExtension)
|
||||
} else if matches!(qualified_name.segments(), ["", "bool"]) {
|
||||
} else if matches!(qualified_name.segments(), ["" | "builtins", "bool"])
|
||||
{
|
||||
Some(typing::Callable::Bool)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -229,6 +229,31 @@ impl<'a> Importer<'a> {
|
|||
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
|
||||
}
|
||||
|
||||
/// For a given builtin symbol, determine whether an [`Edit`] is necessary to make the symbol
|
||||
/// available in the current scope. For example, if `zip` has been overridden in the relevant
|
||||
/// scope, the `builtins` module will need to be imported in order for a `Fix` to reference
|
||||
/// `zip`; but otherwise, that won't be necessary.
|
||||
///
|
||||
/// Returns a two-item tuple. The first item is either `Some(Edit)` (indicating) that an
|
||||
/// edit is necessary to make the symbol available, or `None`, indicating that the symbol has
|
||||
/// not been overridden in the current scope. The second item in the tuple is the bound name
|
||||
/// of the symbol.
|
||||
///
|
||||
/// Attempts to reuse existing imports when possible.
|
||||
pub(crate) fn get_or_import_builtin_symbol(
|
||||
&self,
|
||||
symbol: &str,
|
||||
at: TextSize,
|
||||
semantic: &SemanticModel,
|
||||
) -> Result<(Option<Edit>, String), ResolutionError> {
|
||||
if semantic.is_builtin(symbol) {
|
||||
return Ok((None, symbol.to_string()));
|
||||
}
|
||||
let (import_edit, binding) =
|
||||
self.get_or_import_symbol(&ImportRequest::import("builtins", symbol), at, semantic)?;
|
||||
Ok((Some(import_edit), binding))
|
||||
}
|
||||
|
||||
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
|
||||
fn find_symbol(
|
||||
symbol: &ImportRequest,
|
||||
|
|
|
@ -63,7 +63,7 @@ fn is_open_sleep_or_subprocess_call(func: &Expr, semantic: &SemanticModel) -> bo
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "open"]
|
||||
["" | "builtins", "open"]
|
||||
| ["time", "sleep"]
|
||||
| [
|
||||
"subprocess",
|
||||
|
|
|
@ -24,23 +24,13 @@ pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticMode
|
|||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ {
|
||||
elts.iter().any(|type_| {
|
||||
semantic
|
||||
.resolve_qualified_name(type_)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "Exception" | "BaseException"]
|
||||
)
|
||||
})
|
||||
.resolve_builtin_symbol(type_)
|
||||
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||
})
|
||||
} else {
|
||||
semantic
|
||||
.resolve_qualified_name(type_)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "Exception" | "BaseException"]
|
||||
)
|
||||
})
|
||||
.resolve_builtin_symbol(type_)
|
||||
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -95,24 +95,22 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(exception) =
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(arg)
|
||||
.and_then(|qualified_name| match qualified_name.segments() {
|
||||
["", "Exception"] => Some(ExceptionKind::Exception),
|
||||
["", "BaseException"] => Some(ExceptionKind::BaseException),
|
||||
_ => None,
|
||||
})
|
||||
else {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let exception = match builtin_symbol {
|
||||
"Exception" => ExceptionKind::Exception,
|
||||
"BaseException" => ExceptionKind::BaseException,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
|
||||
{
|
||||
AssertionKind::AssertRaises
|
||||
} else if checker
|
||||
.semantic()
|
||||
} else if semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]))
|
||||
&& arguments.find_keyword("match").is_none()
|
||||
|
|
|
@ -54,12 +54,6 @@ pub(crate) fn getattr_with_constant(
|
|||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
||||
return;
|
||||
};
|
||||
if id != "getattr" {
|
||||
return;
|
||||
}
|
||||
let [obj, arg] = args else {
|
||||
return;
|
||||
};
|
||||
|
@ -75,7 +69,7 @@ pub(crate) fn getattr_with_constant(
|
|||
if is_mangled_private(value.to_str()) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("getattr") {
|
||||
if !checker.semantic().match_builtin_expr(func, "getattr") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,12 +68,6 @@ pub(crate) fn setattr_with_constant(
|
|||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
||||
return;
|
||||
};
|
||||
if id != "setattr" {
|
||||
return;
|
||||
}
|
||||
let [obj, name, value] = args else {
|
||||
return;
|
||||
};
|
||||
|
@ -89,7 +83,7 @@ pub(crate) fn setattr_with_constant(
|
|||
if is_mangled_private(name.to_str()) {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("setattr") {
|
||||
if !checker.semantic().match_builtin_expr(func, "setattr") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
@ -58,12 +57,6 @@ pub(crate) fn unreliable_callable_check(
|
|||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
||||
return;
|
||||
};
|
||||
if !matches!(id.as_str(), "hasattr" | "getattr") {
|
||||
return;
|
||||
}
|
||||
let [obj, attr, ..] = args else {
|
||||
return;
|
||||
};
|
||||
|
@ -73,15 +66,27 @@ pub(crate) fn unreliable_callable_check(
|
|||
if value != "__call__" {
|
||||
return;
|
||||
}
|
||||
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
|
||||
return;
|
||||
};
|
||||
if !matches!(builtins_function, "hasattr" | "getattr") {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnreliableCallableCheck, expr.range());
|
||||
if id == "hasattr" {
|
||||
if checker.semantic().is_builtin("callable") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("callable({})", checker.locator().slice(obj)),
|
||||
if builtins_function == "hasattr" {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"callable",
|
||||
expr.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let binding_edit = Edit::range_replacement(
|
||||
format!("{binding}({})", checker.locator().slice(obj)),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
);
|
||||
Ok(Fix::safe_edits(binding_edit, import_edit))
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
|
|
@ -52,18 +52,18 @@ impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
|
|||
|
||||
/// B905
|
||||
pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() {
|
||||
if id == "zip"
|
||||
&& checker.semantic().is_builtin("zip")
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if semantic.match_builtin_expr(&call.func, "zip")
|
||||
&& call.arguments.find_keyword("strict").is_none()
|
||||
&& !call
|
||||
.arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
|
||||
.any(|arg| is_infinite_iterator(arg, semantic))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(ZipWithoutExplicitStrict, call.range());
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
checker.diagnostics.push(
|
||||
Diagnostic::new(ZipWithoutExplicitStrict, call.range()).with_fix(Fix::applicable_edit(
|
||||
add_argument(
|
||||
"strict=False",
|
||||
&call.arguments,
|
||||
|
@ -81,9 +81,8 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
|||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,4 +31,58 @@ B004.py:5:8: B004 Using `hasattr(x, "__call__")` to test if x is callable is unr
|
|||
|
|
||||
= help: Replace with `callable()`
|
||||
|
||||
B004.py:12:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
||||
|
|
||||
10 | import builtins
|
||||
11 | o = object()
|
||||
12 | if builtins.hasattr(o, "__call__"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B004
|
||||
13 | print("B U G")
|
||||
14 | if builtins.getattr(o, "__call__", False):
|
||||
|
|
||||
= help: Replace with `callable()`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | def still_a_bug():
|
||||
10 10 | import builtins
|
||||
11 11 | o = object()
|
||||
12 |- if builtins.hasattr(o, "__call__"):
|
||||
12 |+ if callable(o):
|
||||
13 13 | print("B U G")
|
||||
14 14 | if builtins.getattr(o, "__call__", False):
|
||||
15 15 | print("B U G")
|
||||
|
||||
B004.py:14:8: B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
||||
|
|
||||
12 | if builtins.hasattr(o, "__call__"):
|
||||
13 | print("B U G")
|
||||
14 | if builtins.getattr(o, "__call__", False):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B004
|
||||
15 | print("B U G")
|
||||
|
|
||||
= help: Replace with `callable()`
|
||||
|
||||
B004.py:24:8: B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
|
||||
|
|
||||
22 | return True
|
||||
23 |
|
||||
24 | if hasattr(o, "__call__"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ B004
|
||||
25 | print("STILL a bug!")
|
||||
|
|
||||
= help: Replace with `callable()`
|
||||
|
||||
ℹ Safe fix
|
||||
1 |+import builtins
|
||||
1 2 | def this_is_a_bug():
|
||||
2 3 | o = object()
|
||||
3 4 | if hasattr(o, "__call__"):
|
||||
--------------------------------------------------------------------------------
|
||||
21 22 | def callable(x):
|
||||
22 23 | return True
|
||||
23 24 |
|
||||
24 |- if hasattr(o, "__call__"):
|
||||
25 |+ if builtins.callable(o):
|
||||
25 26 | print("STILL a bug!")
|
||||
26 27 |
|
||||
27 28 |
|
||||
|
|
|
@ -342,6 +342,8 @@ B009_B010.py:65:1: B009 [*] Do not call `getattr` with a constant attribute valu
|
|||
65 | / getattr(self.
|
||||
66 | | registration.registry, '__name__')
|
||||
| |_____________________________________^ B009
|
||||
67 |
|
||||
68 | import builtins
|
||||
|
|
||||
= help: Replace `getattr` with attribute access
|
||||
|
||||
|
@ -353,5 +355,21 @@ B009_B010.py:65:1: B009 [*] Do not call `getattr` with a constant attribute valu
|
|||
66 |- registration.registry, '__name__')
|
||||
65 |+(self.
|
||||
66 |+ registration.registry).__name__
|
||||
67 67 |
|
||||
68 68 | import builtins
|
||||
69 69 | builtins.getattr(foo, "bar")
|
||||
|
||||
B009_B010.py:69:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
||||
|
|
||||
68 | import builtins
|
||||
69 | builtins.getattr(foo, "bar")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B009
|
||||
|
|
||||
= help: Replace `getattr` with attribute access
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | registration.registry, '__name__')
|
||||
67 67 |
|
||||
68 68 | import builtins
|
||||
69 |-builtins.getattr(foo, "bar")
|
||||
69 |+foo.bar
|
||||
|
|
|
@ -162,6 +162,8 @@ B905.py:24:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
|||
24 |-zip([1, 2, 3], repeat(1, 1))
|
||||
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
|
||||
25 25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
26 26 |
|
||||
27 27 | import builtins
|
||||
|
||||
B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
|
@ -169,6 +171,8 @@ B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
|||
24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
26 |
|
||||
27 | import builtins
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
|
@ -178,5 +182,22 @@ B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
|||
24 24 | zip([1, 2, 3], repeat(1, 1))
|
||||
25 |-zip([1, 2, 3], repeat(1, times=4))
|
||||
25 |+zip([1, 2, 3], repeat(1, times=4), strict=False)
|
||||
26 26 |
|
||||
27 27 | import builtins
|
||||
28 28 | # Still an error even though it uses the qualified name
|
||||
|
||||
B905.py:29:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
|
|
||||
27 | import builtins
|
||||
28 | # Still an error even though it uses the qualified name
|
||||
29 | builtins.zip([1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 |
|
||||
27 27 | import builtins
|
||||
28 28 | # Still an error even though it uses the qualified name
|
||||
29 |-builtins.zip([1, 2, 3])
|
||||
29 |+builtins.zip([1, 2, 3], strict=False)
|
||||
|
|
|
@ -73,7 +73,5 @@ fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||
return false;
|
||||
};
|
||||
semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "locals"]))
|
||||
semantic.match_builtin_expr(func, "locals")
|
||||
}
|
||||
|
|
|
@ -108,11 +108,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
|
|||
arguments: Arguments { keywords, .. },
|
||||
..
|
||||
}) => {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "dict"]))
|
||||
{
|
||||
if checker.semantic().match_builtin_expr(func, "dict") {
|
||||
for keyword in keywords.iter() {
|
||||
if let Some(attr) = &keyword.arg {
|
||||
if is_reserved_attr(attr) {
|
||||
|
|
|
@ -43,17 +43,6 @@ impl AlwaysFixableViolation for UnnecessaryRangeStart {
|
|||
|
||||
/// PIE808
|
||||
pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
// Verify that the call is to the `range` builtin.
|
||||
let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if id != "range" {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("range") {
|
||||
return;
|
||||
};
|
||||
|
||||
// `range` doesn't accept keyword arguments.
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
|
@ -76,6 +65,11 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
|
|||
return;
|
||||
};
|
||||
|
||||
// Verify that the call is to the `range` builtin.
|
||||
if !checker.semantic().match_builtin_expr(&call.func, "range") {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryRangeStart, start.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_argument(
|
||||
|
|
|
@ -7,7 +7,7 @@ PIE808.py:2:7: PIE808 [*] Unnecessary `start` argument in `range`
|
|||
2 | range(0, 10)
|
||||
| ^ PIE808
|
||||
3 |
|
||||
4 | # OK
|
||||
4 | import builtins
|
||||
|
|
||||
= help: Remove `start` argument
|
||||
|
||||
|
@ -16,7 +16,25 @@ PIE808.py:2:7: PIE808 [*] Unnecessary `start` argument in `range`
|
|||
2 |-range(0, 10)
|
||||
2 |+range(10)
|
||||
3 3 |
|
||||
4 4 | # OK
|
||||
5 5 | range(x, 10)
|
||||
4 4 | import builtins
|
||||
5 5 | builtins.range(0, 10)
|
||||
|
||||
PIE808.py:5:16: PIE808 [*] Unnecessary `start` argument in `range`
|
||||
|
|
||||
4 | import builtins
|
||||
5 | builtins.range(0, 10)
|
||||
| ^ PIE808
|
||||
6 |
|
||||
7 | # OK
|
||||
|
|
||||
= help: Remove `start` argument
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | range(0, 10)
|
||||
3 3 |
|
||||
4 4 | import builtins
|
||||
5 |-builtins.range(0, 10)
|
||||
5 |+builtins.range(10)
|
||||
6 6 |
|
||||
7 7 | # OK
|
||||
8 8 | range(x, 10)
|
||||
|
|
|
@ -97,37 +97,32 @@ impl Violation for PPrint {
|
|||
|
||||
/// T201, T203
|
||||
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let mut diagnostic = {
|
||||
let qualified_name = checker.semantic().resolve_qualified_name(&call.func);
|
||||
if qualified_name
|
||||
.as_ref()
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"]))
|
||||
{
|
||||
let semantic = checker.semantic();
|
||||
|
||||
let Some(qualified_name) = semantic.resolve_qualified_name(&call.func) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = match qualified_name.segments() {
|
||||
["" | "builtins", "print"] => {
|
||||
// If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`,
|
||||
// or `"sys.stderr"`), don't trigger T201.
|
||||
if let Some(keyword) = call.arguments.find_keyword("file") {
|
||||
if !keyword.value.is_none_literal_expr() {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&keyword.value)
|
||||
.map_or(true, |qualified_name| {
|
||||
qualified_name.segments() != ["sys", "stdout"]
|
||||
&& qualified_name.segments() != ["sys", "stderr"]
|
||||
})
|
||||
{
|
||||
if semantic.resolve_qualified_name(&keyword.value).map_or(
|
||||
true,
|
||||
|qualified_name| {
|
||||
!matches!(qualified_name.segments(), ["sys", "stdout" | "stderr"])
|
||||
},
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Diagnostic::new(Print, call.func.range())
|
||||
} else if qualified_name
|
||||
.as_ref()
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pprint", "pprint"]))
|
||||
{
|
||||
Diagnostic::new(PPrint, call.func.range())
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
["pprint", "pprint"] => Diagnostic::new(PPrint, call.func.range()),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if !checker.enabled(diagnostic.kind.rule()) {
|
||||
|
@ -135,13 +130,14 @@ pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
|||
}
|
||||
|
||||
// Remove the `print`, if it's a standalone statement.
|
||||
if checker.semantic().current_expression_parent().is_none() {
|
||||
let statement = checker.semantic().current_statement();
|
||||
let parent = checker.semantic().current_statement_parent();
|
||||
if semantic.current_expression_parent().is_none() {
|
||||
let statement = semantic.current_statement();
|
||||
let parent = semantic.current_statement_parent();
|
||||
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
|
||||
diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_parent_id(),
|
||||
)));
|
||||
diagnostic.set_fix(
|
||||
Fix::unsafe_edit(edit)
|
||||
.isolate(Checker::isolation(semantic.current_statement_parent_id())),
|
||||
);
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
|
|
@ -72,11 +72,13 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
|
|||
return;
|
||||
};
|
||||
|
||||
if !checker.semantic().current_scope().kind.is_class() {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if !semantic.current_scope().kind.is_class() {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker.semantic().match_typing_expr(annotation, "Any") {
|
||||
if semantic.match_typing_expr(annotation, "Any") {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
AnyEqNeAnnotation {
|
||||
method_name: name.to_string(),
|
||||
|
@ -84,12 +86,15 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
|
|||
annotation.range(),
|
||||
);
|
||||
// Ex) `def __eq__(self, obj: Any): ...`
|
||||
if checker.semantic().is_builtin("object") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"object".to_string(),
|
||||
annotation.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"object",
|
||||
annotation.start(),
|
||||
semantic,
|
||||
)?;
|
||||
let binding_edit = Edit::range_replacement(binding, annotation.range());
|
||||
Ok(Fix::safe_edits(binding_edit, import_edit))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,12 +181,15 @@ fn check_short_args_list(checker: &mut Checker, parameters: &Parameters, func_ki
|
|||
annotation.range(),
|
||||
);
|
||||
|
||||
if checker.semantic().is_builtin("object") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"object".to_string(),
|
||||
annotation.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"object",
|
||||
annotation.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let binding_edit = Edit::range_replacement(binding, annotation.range());
|
||||
Ok(Fix::safe_edits(binding_edit, import_edit))
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
@ -213,7 +216,9 @@ fn check_positional_args(
|
|||
|
||||
let validations: [(ErrorKind, AnnotationValidator); 3] = [
|
||||
(ErrorKind::FirstArgBadAnnotation, is_base_exception_type),
|
||||
(ErrorKind::SecondArgBadAnnotation, is_base_exception),
|
||||
(ErrorKind::SecondArgBadAnnotation, |expr, semantic| {
|
||||
semantic.match_builtin_expr(expr, "BaseException")
|
||||
}),
|
||||
(ErrorKind::ThirdArgBadAnnotation, is_traceback_type),
|
||||
];
|
||||
|
||||
|
@ -322,19 +327,6 @@ fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is `BaseException`.
|
||||
fn is_base_exception(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.as_ref()
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["" | "builtins", "BaseException"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
|
||||
fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic
|
||||
|
@ -351,15 +343,8 @@ fn is_base_exception_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
return false;
|
||||
};
|
||||
|
||||
if semantic.match_typing_expr(value, "Type")
|
||||
|| semantic
|
||||
.resolve_qualified_name(value)
|
||||
.as_ref()
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["" | "builtins", "type"])
|
||||
})
|
||||
{
|
||||
is_base_exception(slice, semantic)
|
||||
if semantic.match_typing_expr(value, "Type") || semantic.match_builtin_expr(value, "type") {
|
||||
semantic.match_builtin_expr(slice, "BaseException")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
|
@ -96,20 +96,20 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
|
|||
let mut has_complex = false;
|
||||
let mut has_int = false;
|
||||
|
||||
let mut func = |expr: &Expr, _parent: &Expr| {
|
||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) else {
|
||||
let mut find_numeric_type = |expr: &Expr, _parent: &Expr| {
|
||||
let Some(builtin_type) = checker.semantic().resolve_builtin_symbol(expr) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match qualified_name.segments() {
|
||||
["" | "builtins", "int"] => has_int = true,
|
||||
["" | "builtins", "float"] => has_float = true,
|
||||
["" | "builtins", "complex"] => has_complex = true,
|
||||
_ => (),
|
||||
match builtin_type {
|
||||
"int" => has_int = true,
|
||||
"float" => has_float = true,
|
||||
"complex" => has_complex = true,
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
traverse_union(&mut func, checker.semantic(), annotation);
|
||||
traverse_union(&mut find_numeric_type, checker.semantic(), annotation);
|
||||
|
||||
if has_complex {
|
||||
if has_float {
|
||||
|
|
|
@ -78,13 +78,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
|
|||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(returns)
|
||||
.map_or(true, |qualified_name| {
|
||||
!matches!(qualified_name.segments(), ["" | "builtins", "str"])
|
||||
})
|
||||
{
|
||||
if !checker.semantic().match_builtin_expr(returns, "str") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,46 +53,34 @@ impl Violation for UnnecessaryTypeUnion {
|
|||
|
||||
/// PYI055
|
||||
pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
// The `|` operator isn't always safe to allow to runtime-evaluated annotations.
|
||||
if checker.semantic().execution_context().is_runtime() {
|
||||
if semantic.execution_context().is_runtime() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`
|
||||
let subscript = union.as_subscript_expr();
|
||||
if subscript.is_some_and(|subscript| {
|
||||
!checker
|
||||
.semantic()
|
||||
.match_typing_expr(&subscript.value, "Union")
|
||||
}) {
|
||||
if subscript.is_some_and(|subscript| !semantic.match_typing_expr(&subscript.value, "Union")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut type_exprs = Vec::new();
|
||||
let mut other_exprs = Vec::new();
|
||||
let mut type_exprs: Vec<&Expr> = Vec::new();
|
||||
let mut other_exprs: Vec<&Expr> = Vec::new();
|
||||
|
||||
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| {
|
||||
let subscript = expr.as_subscript_expr();
|
||||
|
||||
if subscript.is_none() {
|
||||
other_exprs.push(expr);
|
||||
} else {
|
||||
let unwrapped = subscript.unwrap();
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(unwrapped.value.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["" | "builtins", "type"])
|
||||
})
|
||||
{
|
||||
type_exprs.push(unwrapped.slice.as_ref());
|
||||
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| match expr {
|
||||
Expr::Subscript(ast::ExprSubscript { slice, value, .. }) => {
|
||||
if semantic.match_builtin_expr(value, "type") {
|
||||
type_exprs.push(slice);
|
||||
} else {
|
||||
other_exprs.push(expr);
|
||||
}
|
||||
}
|
||||
_ => other_exprs.push(expr),
|
||||
};
|
||||
|
||||
traverse_union(&mut collect_type_exprs, checker.semantic(), union);
|
||||
traverse_union(&mut collect_type_exprs, semantic, union);
|
||||
|
||||
if type_exprs.len() > 1 {
|
||||
let type_members: Vec<String> = type_exprs
|
||||
|
@ -109,7 +97,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
|
|||
union.range(),
|
||||
);
|
||||
|
||||
if checker.semantic().is_builtin("type") {
|
||||
if semantic.is_builtin("type") {
|
||||
let content = if let Some(subscript) = subscript {
|
||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||
value: Box::new(Expr::Name(ast::ExprName {
|
||||
|
|
|
@ -301,7 +301,7 @@ pub(crate) fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> {
|
|||
/// If `call` is an `isinstance()` call, return its target.
|
||||
fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> {
|
||||
// Verify that this is an `isinstance` call.
|
||||
let Expr::Call(ast::ExprCall {
|
||||
let ast::ExprCall {
|
||||
func,
|
||||
arguments:
|
||||
Arguments {
|
||||
|
@ -310,23 +310,14 @@ fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option<
|
|||
range: _,
|
||||
},
|
||||
range: _,
|
||||
}) = &call
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
} = call.as_call_expr()?;
|
||||
if args.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
if !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Expr::Name(ast::ExprName { id: func_name, .. }) = func.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if func_name != "isinstance" {
|
||||
return None;
|
||||
}
|
||||
if !semantic.is_builtin("isinstance") {
|
||||
if !semantic.match_builtin_expr(func, "isinstance") {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
@ -113,26 +113,28 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool {
|
|||
}
|
||||
|
||||
/// Return `true` if `func` is the builtin `open` or `pathlib.Path(...).open`.
|
||||
fn is_open(checker: &mut Checker, func: &Expr) -> bool {
|
||||
match func {
|
||||
// pathlib.Path(...).open()
|
||||
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
|
||||
match value.as_ref() {
|
||||
Expr::Call(ast::ExprCall { func, .. }) => checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["pathlib", "Path"])
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn is_open(semantic: &SemanticModel, func: &Expr) -> bool {
|
||||
// open(...)
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
id.as_str() == "open" && checker.semantic().is_builtin("open")
|
||||
if semantic.match_builtin_expr(func, "open") {
|
||||
return true;
|
||||
}
|
||||
_ => false,
|
||||
|
||||
// pathlib.Path(...).open()
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
if attr != "open" {
|
||||
return false;
|
||||
}
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func: value_func, ..
|
||||
}) = &**value
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
semantic
|
||||
.resolve_qualified_name(value_func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pathlib", "Path"]))
|
||||
}
|
||||
|
||||
/// Return `true` if the current expression is followed by a `close` call.
|
||||
|
@ -161,27 +163,29 @@ fn is_closed(semantic: &SemanticModel) -> bool {
|
|||
|
||||
/// SIM115
|
||||
pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) {
|
||||
if !is_open(checker, func) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if !is_open(semantic, func) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `open("foo.txt").close()`
|
||||
if is_closed(checker.semantic()) {
|
||||
if is_closed(semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `with open("foo.txt") as f: ...`
|
||||
if checker.semantic().current_statement().is_with_stmt() {
|
||||
if semantic.current_statement().is_with_stmt() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `with contextlib.ExitStack() as exit_stack: ...`
|
||||
if match_exit_stack(checker.semantic()) {
|
||||
if match_exit_stack(semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `with contextlib.AsyncExitStack() as exit_stack: ...`
|
||||
if match_async_exit_stack(checker.semantic()) {
|
||||
if match_async_exit_stack(semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -55,16 +55,11 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla
|
|||
return;
|
||||
};
|
||||
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if bases.iter().any(|base| {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(map_subscript(base))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["" | "builtins", "tuple"])
|
||||
|| checker
|
||||
.semantic()
|
||||
.match_typing_qualified_name(&qualified_name, "Tuple")
|
||||
})
|
||||
let base = map_subscript(base);
|
||||
semantic.match_builtin_expr(base, "tuple") || semantic.match_typing_expr(base, "Tuple")
|
||||
}) {
|
||||
if !has_slots(&class.body) {
|
||||
checker
|
||||
|
|
|
@ -22,4 +22,11 @@ SLOT001.py:16:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
|||
17 | pass
|
||||
|
|
||||
|
||||
|
||||
SLOT001.py:26:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
||||
|
|
||||
24 | import builtins
|
||||
25 |
|
||||
26 | class AlsoBad(builtins.tuple[int, int]): # SLOT001
|
||||
| ^^^^^^^ SLOT001
|
||||
27 | pass
|
||||
|
|
||||
|
|
|
@ -69,11 +69,7 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr, body: &[
|
|||
return;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !(id == "list" && checker.semantic().is_builtin("list")) {
|
||||
if !checker.semantic().match_builtin_expr(func, "list") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -221,4 +221,20 @@ PERF101.py:69:10: PERF101 [*] Do not cast an iterable to `list` before iterating
|
|||
71 71 |
|
||||
72 72 | for i in list(foo_list): # OK
|
||||
|
||||
PERF101.py:86:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
||||
|
|
||||
84 | import builtins
|
||||
85 |
|
||||
86 | for i in builtins.list(nested_tuple): # PERF101
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF101
|
||||
87 | pass
|
||||
|
|
||||
= help: Remove `list()` cast
|
||||
|
||||
ℹ Safe fix
|
||||
83 83 |
|
||||
84 84 | import builtins
|
||||
85 85 |
|
||||
86 |-for i in builtins.list(nested_tuple): # PERF101
|
||||
86 |+for i in nested_tuple: # PERF101
|
||||
87 87 | pass
|
||||
|
|
|
@ -76,11 +76,9 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
|||
continue;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if !(id == "type" && checker.semantic().is_builtin("type")) {
|
||||
if !semantic.match_builtin_expr(func, "type") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -90,11 +88,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
|||
func, arguments, ..
|
||||
}) => {
|
||||
// Ex) `type(obj) is type(1)`
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if id == "type" && checker.semantic().is_builtin("type") {
|
||||
if semantic.match_builtin_expr(func, "type") {
|
||||
// Allow comparison for types which are not obvious.
|
||||
if arguments
|
||||
.args
|
||||
|
@ -112,8 +106,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
|||
}
|
||||
Expr::Attribute(ast::ExprAttribute { value, .. }) => {
|
||||
// Ex) `type(obj) is types.NoneType`
|
||||
if checker
|
||||
.semantic()
|
||||
if semantic
|
||||
.resolve_qualified_name(value.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["types", ..])
|
||||
|
@ -141,7 +134,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
|||
| "dict"
|
||||
| "set"
|
||||
| "memoryview"
|
||||
) && checker.semantic().is_builtin(id)
|
||||
) && semantic.is_builtin(id)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TypeComparison {
|
||||
|
@ -188,20 +181,17 @@ fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
Expr::Call(ast::ExprCall {
|
||||
func, arguments, ..
|
||||
}) => {
|
||||
// Ex) `type(obj) == type(1)`
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !(id == "type" && semantic.is_builtin("type")) {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Allow comparison for types which are not obvious.
|
||||
arguments
|
||||
if !arguments
|
||||
.args
|
||||
.first()
|
||||
.is_some_and(|arg| !arg.is_name_expr() && !arg.is_none_literal_expr())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) `type(obj) == type(1)`
|
||||
semantic.match_builtin_expr(func, "type")
|
||||
}
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
// Ex) `type(obj) == int`
|
||||
|
|
|
@ -167,4 +167,11 @@ E721.py:117:12: E721 Do not compare types, use `isinstance()`
|
|||
118 | ...
|
||||
|
|
||||
|
||||
|
||||
E721.py:144:4: E721 Do not compare types, use `isinstance()`
|
||||
|
|
||||
142 | import builtins
|
||||
143 |
|
||||
144 | if builtins.type(res) == memoryview: # E721
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
|
||||
145 | pass
|
||||
|
|
||||
|
|
|
@ -134,6 +134,15 @@ E721.py:140:1: E721 Use `is` and `is not` for type comparisons, or `isinstance()
|
|||
139 | #: E721
|
||||
140 | dtype == float
|
||||
| ^^^^^^^^^^^^^^ E721
|
||||
141 |
|
||||
142 | import builtins
|
||||
|
|
||||
|
||||
|
||||
E721.py:144:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
||||
|
|
||||
142 | import builtins
|
||||
143 |
|
||||
144 | if builtins.type(res) == memoryview: # E721
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E721
|
||||
145 | pass
|
||||
|
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
@ -58,16 +57,9 @@ impl Violation for InvalidPrintSyntax {
|
|||
|
||||
/// F633
|
||||
pub(crate) fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = &left else {
|
||||
return;
|
||||
};
|
||||
if id != "print" {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("print") {
|
||||
return;
|
||||
};
|
||||
if checker.semantic().match_builtin_expr(left, "print") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(InvalidPrintSyntax, left.range()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,11 +75,16 @@ pub(crate) fn raise_not_implemented(checker: &mut Checker, expr: &Expr) {
|
|||
return;
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range());
|
||||
if checker.semantic().is_builtin("NotImplementedError") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"NotImplementedError".to_string(),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"NotImplementedError",
|
||||
expr.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, expr.range()),
|
||||
import_edit,
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
|
|
@ -31,5 +31,27 @@ F901.py:6:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr
|
|||
5 5 | def g() -> None:
|
||||
6 |- raise NotImplemented
|
||||
6 |+ raise NotImplementedError
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 9 | def h() -> None:
|
||||
|
||||
F901.py:11:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedError`
|
||||
|
|
||||
9 | def h() -> None:
|
||||
10 | NotImplementedError = "foo"
|
||||
11 | raise NotImplemented
|
||||
| ^^^^^^^^^^^^^^ F901
|
||||
|
|
||||
= help: Use `raise NotImplementedError`
|
||||
|
||||
ℹ Safe fix
|
||||
1 |+import builtins
|
||||
1 2 | def f() -> None:
|
||||
2 3 | raise NotImplemented()
|
||||
3 4 |
|
||||
--------------------------------------------------------------------------------
|
||||
8 9 |
|
||||
9 10 | def h() -> None:
|
||||
10 11 | NotImplementedError = "foo"
|
||||
11 |- raise NotImplemented
|
||||
12 |+ raise builtins.NotImplementedError
|
||||
|
|
|
@ -85,23 +85,22 @@ enum Kind {
|
|||
|
||||
/// If a function is a call to `open`, returns the kind of `open` call.
|
||||
fn is_open(func: &Expr, semantic: &SemanticModel) -> Option<Kind> {
|
||||
match func {
|
||||
// Ex) `pathlib.Path(...).open(...)`
|
||||
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
|
||||
match value.as_ref() {
|
||||
Expr::Call(ast::ExprCall { func, .. }) => semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["pathlib", "Path"])
|
||||
})
|
||||
.then_some(Kind::Pathlib),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
// Ex) `open(...)`
|
||||
Expr::Name(ast::ExprName { id, .. }) => {
|
||||
(id.as_str() == "open" && semantic.is_builtin("open")).then_some(Kind::Builtin)
|
||||
if semantic.match_builtin_expr(func, "open") {
|
||||
return Some(Kind::Builtin);
|
||||
}
|
||||
|
||||
// Ex) `pathlib.Path(...).open(...)`
|
||||
let ast::ExprAttribute { attr, value, .. } = func.as_attribute_expr()?;
|
||||
if attr != "open" {
|
||||
return None;
|
||||
}
|
||||
let ast::ExprCall {
|
||||
func: value_func, ..
|
||||
} = value.as_call_expr()?;
|
||||
let qualified_name = semantic.resolve_qualified_name(value_func)?;
|
||||
match qualified_name.segments() {
|
||||
["pathlib", "Path"] => Some(Kind::Pathlib),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,27 +96,20 @@ impl std::fmt::Display for Nan {
|
|||
|
||||
/// Returns `true` if the expression is a call to `float("NaN")`.
|
||||
fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Call(call) = expr else {
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments: ast::Arguments { args, keywords, .. },
|
||||
..
|
||||
}) = expr
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if id.as_str() != "float" {
|
||||
if !keywords.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [arg] = call.arguments.args.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
|
||||
let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**args else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
@ -127,9 +120,5 @@ fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
if !semantic.is_builtin("float") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
semantic.match_builtin_expr(func, "float")
|
||||
}
|
||||
|
|
|
@ -68,15 +68,10 @@ impl MinMax {
|
|||
if !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
||||
return None;
|
||||
};
|
||||
if id.as_str() == "min" && semantic.is_builtin("min") {
|
||||
Some(MinMax::Min)
|
||||
} else if id.as_str() == "max" && semantic.is_builtin("max") {
|
||||
Some(MinMax::Max)
|
||||
} else {
|
||||
None
|
||||
match semantic.resolve_builtin_symbol(func)? {
|
||||
"min" => Some(Self::Min),
|
||||
"max" => Some(Self::Max),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,14 +61,15 @@ impl Violation for NonSlotAssignment {
|
|||
|
||||
/// E0237
|
||||
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
// If the class inherits from another class (aside from `object`), then it's possible that
|
||||
// the parent class defines the relevant `__slots__`.
|
||||
if !class_def.bases().iter().all(|base| {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(base)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "object"]))
|
||||
}) {
|
||||
if !class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.all(|base| semantic.match_builtin_expr(base, "object"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use ruff_python_ast::{self as ast, Decorator, Expr, Parameters, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{identifier::Identifier, Decorator, Parameters, Stmt};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
|
@ -53,9 +51,10 @@ pub(crate) fn property_with_parameters(
|
|||
decorator_list: &[Decorator],
|
||||
parameters: &Parameters,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
if !decorator_list
|
||||
.iter()
|
||||
.any(|decorator| matches!(&decorator.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property"))
|
||||
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "property"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -66,7 +65,6 @@ pub(crate) fn property_with_parameters(
|
|||
.chain(¶meters.kwonlyargs)
|
||||
.count()
|
||||
> 1
|
||||
&& checker.semantic().is_builtin("property")
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
|
|
|
@ -92,14 +92,11 @@ pub(crate) fn repeated_isinstance_calls(
|
|||
else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(func.as_ref(), Expr::Name(ast::ExprName { id, .. }) if id == "isinstance") {
|
||||
continue;
|
||||
}
|
||||
let [obj, types] = &args[..] else {
|
||||
continue;
|
||||
};
|
||||
if !checker.semantic().is_builtin("isinstance") {
|
||||
return;
|
||||
if !checker.semantic().match_builtin_expr(func, "isinstance") {
|
||||
continue;
|
||||
}
|
||||
let (num_calls, matches) = obj_to_types
|
||||
.entry(obj.into())
|
||||
|
|
|
@ -124,16 +124,6 @@ fn enumerate_items<'a>(
|
|||
func, arguments, ..
|
||||
} = call_expr.as_call_expr()?;
|
||||
|
||||
// Check that the function is the `enumerate` builtin.
|
||||
if !semantic
|
||||
.resolve_qualified_name(func.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["builtins" | "", "enumerate"])
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let Expr::Tuple(ast::ExprTuple { elts, .. }) = tuple_expr else {
|
||||
return None;
|
||||
};
|
||||
|
@ -161,6 +151,11 @@ fn enumerate_items<'a>(
|
|||
return None;
|
||||
};
|
||||
|
||||
// Check that the function is the `enumerate` builtin.
|
||||
if !semantic.match_builtin_expr(func, "enumerate") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((sequence, index_name, value_name))
|
||||
}
|
||||
|
||||
|
|
|
@ -80,3 +80,11 @@ nan_comparison.py:47:9: PLW0177 Comparing against a NaN value; use `np.isnan` in
|
|||
| ^^^^^^^ PLW0177
|
||||
48 | pass
|
||||
|
|
||||
|
||||
nan_comparison.py:53:9: PLW0177 Comparing against a NaN value; use `math.isnan` instead
|
||||
|
|
||||
52 | # PLW0117
|
||||
53 | if x == builtins.float("nan"):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ PLW0177
|
||||
54 | pass
|
||||
|
|
||||
|
|
|
@ -106,6 +106,13 @@ bad_open_mode.py:34:25: PLW1501 `rwx` is not a valid mode for `open`
|
|||
33 | pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
|
||||
34 | pathlib.Path(NAME).open("rwx", encoding="utf-8") # [bad-open-mode]
|
||||
| ^^^^^ PLW1501
|
||||
35 |
|
||||
36 | import builtins
|
||||
|
|
||||
|
||||
|
||||
bad_open_mode.py:37:21: PLW1501 `Ua` is not a valid mode for `open`
|
||||
|
|
||||
36 | import builtins
|
||||
37 | builtins.open(NAME, "Ua", encoding="utf-8")
|
||||
| ^^^^ PLW1501
|
||||
|
|
||||
|
|
|
@ -283,6 +283,8 @@ nested_min_max.py:41:1: PLW3301 [*] Nested `max` calls can be flattened
|
|||
40 | # Starred argument should be copied as it is.
|
||||
41 | max(1, max(*a))
|
||||
| ^^^^^^^^^^^^^^^ PLW3301
|
||||
42 |
|
||||
43 | import builtins
|
||||
|
|
||||
= help: Flatten nested `max` calls
|
||||
|
||||
|
@ -292,5 +294,21 @@ nested_min_max.py:41:1: PLW3301 [*] Nested `max` calls can be flattened
|
|||
40 40 | # Starred argument should be copied as it is.
|
||||
41 |-max(1, max(*a))
|
||||
41 |+max(1, *a)
|
||||
42 42 |
|
||||
43 43 | import builtins
|
||||
44 44 | builtins.min(1, min(2, 3))
|
||||
|
||||
nested_min_max.py:44:1: PLW3301 [*] Nested `min` calls can be flattened
|
||||
|
|
||||
43 | import builtins
|
||||
44 | builtins.min(1, min(2, 3))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
|
||||
|
|
||||
= help: Flatten nested `min` calls
|
||||
|
||||
ℹ Unsafe fix
|
||||
41 41 | max(1, max(*a))
|
||||
42 42 |
|
||||
43 43 | import builtins
|
||||
44 |-builtins.min(1, min(2, 3))
|
||||
44 |+builtins.min(1, 2, 3)
|
||||
|
|
|
@ -53,12 +53,17 @@ pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
|||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["io", "open"]))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(OpenAlias, expr.range());
|
||||
if checker.semantic().is_builtin("open") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"open".to_string(),
|
||||
func.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"open",
|
||||
expr.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, func.range()),
|
||||
import_edit,
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,19 +61,14 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "EnvironmentError" | "IOError" | "WindowsError"]
|
||||
| ["mmap" | "select" | "socket" | "os", "error"]
|
||||
[
|
||||
"" | "builtins",
|
||||
"EnvironmentError" | "IOError" | "WindowsError"
|
||||
] | ["mmap" | "select" | "socket" | "os", "error"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if an [`Expr`] is `OSError`.
|
||||
fn is_os_error(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "OSError"]))
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
|
||||
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
|
@ -82,19 +77,25 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
|||
},
|
||||
target.range(),
|
||||
);
|
||||
if checker.semantic().is_builtin("OSError") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"OSError".to_string(),
|
||||
target.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"OSError",
|
||||
target.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, target.range()),
|
||||
import_edit,
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
||||
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range());
|
||||
if checker.semantic().is_builtin("OSError") {
|
||||
let semantic = checker.semantic();
|
||||
if semantic.is_builtin("OSError") {
|
||||
// Filter out any `OSErrors` aliases.
|
||||
let mut remaining: Vec<Expr> = tuple
|
||||
.elts
|
||||
|
@ -112,7 +113,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
|
|||
if tuple
|
||||
.elts
|
||||
.iter()
|
||||
.all(|elt| !is_os_error(elt, checker.semantic()))
|
||||
.all(|elt| !semantic.match_builtin_expr(elt, "OSError"))
|
||||
{
|
||||
let node = ast::ExprName {
|
||||
id: "OSError".into(),
|
||||
|
|
|
@ -111,7 +111,7 @@ pub(crate) fn replace_str_enum(checker: &mut Checker, class_def: &ast::StmtClass
|
|||
for base in arguments.args.iter() {
|
||||
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) {
|
||||
match qualified_name.segments() {
|
||||
["", "str"] => inherits_str = true,
|
||||
["" | "builtins", "str"] => inherits_str = true,
|
||||
["enum", "Enum"] => inherits_enum = true,
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -81,13 +81,6 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel, target_version: PythonVersion
|
|||
})
|
||||
}
|
||||
|
||||
/// Return `true` if an [`Expr`] is `TimeoutError`.
|
||||
fn is_timeout_error(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
semantic
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "TimeoutError"]))
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
|
||||
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
|
@ -96,19 +89,25 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
|||
},
|
||||
target.range(),
|
||||
);
|
||||
if checker.semantic().is_builtin("TimeoutError") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"TimeoutError".to_string(),
|
||||
target.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"TimeoutError",
|
||||
target.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, target.range()),
|
||||
import_edit,
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
||||
let mut diagnostic = Diagnostic::new(TimeoutErrorAlias { name: None }, tuple.range());
|
||||
if checker.semantic().is_builtin("TimeoutError") {
|
||||
let semantic = checker.semantic();
|
||||
if semantic.is_builtin("TimeoutError") {
|
||||
// Filter out any `TimeoutErrors` aliases.
|
||||
let mut remaining: Vec<Expr> = tuple
|
||||
.elts
|
||||
|
@ -126,7 +125,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
|
|||
if tuple
|
||||
.elts
|
||||
.iter()
|
||||
.all(|elt| !is_timeout_error(elt, checker.semantic()))
|
||||
.all(|elt| !semantic.match_builtin_expr(elt, "TimeoutError"))
|
||||
{
|
||||
let node = ast::ExprName {
|
||||
id: "TimeoutError".into(),
|
||||
|
|
|
@ -58,19 +58,16 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr,
|
|||
let [arg] = args else {
|
||||
return;
|
||||
};
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "type"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(primitive) = Primitive::from_expr(arg) else {
|
||||
return;
|
||||
};
|
||||
let semantic = checker.semantic();
|
||||
if !semantic.match_builtin_expr(func, "type") {
|
||||
return;
|
||||
}
|
||||
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range());
|
||||
let builtin = primitive.builtin();
|
||||
if checker.semantic().is_builtin(&builtin) {
|
||||
if semantic.is_builtin(&builtin) {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
pad(primitive.builtin(), expr.range(), checker.locator()),
|
||||
expr.range(),
|
||||
|
|
|
@ -57,12 +57,17 @@ pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) {
|
|||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range());
|
||||
if checker.semantic().is_builtin("str") {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
"str".to_string(),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||
"str",
|
||||
expr.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, expr.range()),
|
||||
import_edit,
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
@ -51,13 +51,7 @@ pub(crate) fn useless_object_inheritance(checker: &mut Checker, class_def: &ast:
|
|||
};
|
||||
|
||||
for base in arguments.args.iter() {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = base else {
|
||||
continue;
|
||||
};
|
||||
if id != "object" {
|
||||
continue;
|
||||
}
|
||||
if !checker.semantic().is_builtin("object") {
|
||||
if !checker.semantic().match_builtin_expr(base, "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -490,4 +490,20 @@ UP004.py:146:9: UP004 [*] Class `A` inherits from `object`
|
|||
148 148 |
|
||||
149 149 |
|
||||
|
||||
UP004.py:159:15: UP004 [*] Class `Unusual` inherits from `object`
|
||||
|
|
||||
157 | import builtins
|
||||
158 |
|
||||
159 | class Unusual(builtins.object):
|
||||
| ^^^^^^^^^^^^^^^ UP004
|
||||
160 | ...
|
||||
|
|
||||
= help: Remove `object` inheritance
|
||||
|
||||
ℹ Safe fix
|
||||
156 156 |
|
||||
157 157 | import builtins
|
||||
158 158 |
|
||||
159 |-class Unusual(builtins.object):
|
||||
159 |+class Unusual:
|
||||
160 160 | ...
|
||||
|
|
|
@ -20,7 +20,7 @@ UP020.py:3:6: UP020 [*] Use builtin `open`
|
|||
5 5 |
|
||||
6 6 | from io import open
|
||||
|
||||
UP020.py:8:6: UP020 Use builtin `open`
|
||||
UP020.py:8:6: UP020 [*] Use builtin `open`
|
||||
|
|
||||
6 | from io import open
|
||||
7 |
|
||||
|
@ -30,4 +30,12 @@ UP020.py:8:6: UP020 Use builtin `open`
|
|||
|
|
||||
= help: Replace with builtin `open`
|
||||
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | print(f.read())
|
||||
5 5 |
|
||||
6 6 | from io import open
|
||||
7 |+import builtins
|
||||
7 8 |
|
||||
8 |-with open("f.txt") as f:
|
||||
9 |+with builtins.open("f.txt") as f:
|
||||
9 10 | print(f.read())
|
||||
|
|
|
@ -137,10 +137,6 @@ fn find_file_open<'a>(
|
|||
..
|
||||
} = item.context_expr.as_call_expr()?;
|
||||
|
||||
if func.as_name_expr()?.id != "open" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let var = item.optional_vars.as_deref()?.as_name_expr()?;
|
||||
|
||||
// Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`,
|
||||
|
@ -152,6 +148,10 @@ fn find_file_open<'a>(
|
|||
return None;
|
||||
}
|
||||
|
||||
if !semantic.match_builtin_expr(func, "open") {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Match positional arguments, get filename and mode.
|
||||
let (filename, pos_mode) = match_open_args(args)?;
|
||||
|
||||
|
|
|
@ -97,15 +97,6 @@ pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
|
|||
return;
|
||||
};
|
||||
|
||||
// Ensure that we're performing a `bin(...)`.
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["" | "builtins", "bin"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !arguments.keywords.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
@ -113,6 +104,11 @@ pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
|
|||
return;
|
||||
};
|
||||
|
||||
// Ensure that we're performing a `bin(...)`.
|
||||
if !checker.semantic().match_builtin_expr(func, "bin") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract, e.g., `x` in `bin(x)`.
|
||||
let literal_text = checker.locator().slice(arg);
|
||||
|
||||
|
|
|
@ -66,13 +66,7 @@ impl AlwaysFixableViolation for IntOnSlicedStr {
|
|||
|
||||
pub(crate) fn int_on_sliced_str(checker: &mut Checker, call: &ExprCall) {
|
||||
// Verify that the function is `int`.
|
||||
let Expr::Name(name) = call.func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if name.id.as_str() != "int" {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("int") {
|
||||
if !checker.semantic().match_builtin_expr(&call.func, "int") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -141,17 +141,11 @@ fn extract_name_from_reversed<'a>(
|
|||
return None;
|
||||
};
|
||||
|
||||
let arg = func
|
||||
.as_name_expr()
|
||||
.is_some_and(|name| name.id == "reversed")
|
||||
.then(|| arg.as_name_expr())
|
||||
.flatten()?;
|
||||
|
||||
if !semantic.is_builtin("reversed") {
|
||||
if !semantic.match_builtin_expr(func, "reversed") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(arg)
|
||||
arg.as_name_expr()
|
||||
}
|
||||
|
||||
/// Given a slice expression, returns the inner argument if it's a reversed slice.
|
||||
|
|
|
@ -70,12 +70,7 @@ impl Violation for PrintEmptyString {
|
|||
|
||||
/// FURB105
|
||||
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.as_ref()
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"]))
|
||||
{
|
||||
if !checker.semantic().match_builtin_expr(&call.func, "print") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ impl Violation for ReadWholeFile {
|
|||
/// FURB101
|
||||
pub(crate) fn read_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
||||
// `async` check here is more of a precaution.
|
||||
if with.is_async || !checker.semantic().is_builtin("open") {
|
||||
if with.is_async {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -102,22 +102,16 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
|
|||
return;
|
||||
};
|
||||
|
||||
// Check that the function is the `enumerate` builtin.
|
||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if id != "enumerate" {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("enumerate") {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the first argument, which is the sequence to iterate over.
|
||||
let Some(Expr::Name(sequence)) = arguments.args.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check that the function is the `enumerate` builtin.
|
||||
if !checker.semantic().match_builtin_expr(func, "enumerate") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the index and value are used.
|
||||
match (
|
||||
checker.semantic().is_unused(index),
|
||||
|
|
|
@ -71,20 +71,19 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
|||
_ => return,
|
||||
};
|
||||
|
||||
let semantic = checker.semantic();
|
||||
|
||||
// The value must be either `decimal.Decimal` or `fractions.Fraction`.
|
||||
let Some(constructor) =
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(value)
|
||||
.and_then(|qualified_name| match qualified_name.segments() {
|
||||
["decimal", "Decimal"] => Some(Constructor::Decimal),
|
||||
["fractions", "Fraction"] => Some(Constructor::Fraction),
|
||||
_ => None,
|
||||
})
|
||||
else {
|
||||
let Some(qualified_name) = semantic.resolve_qualified_name(value) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let constructor = match qualified_name.segments() {
|
||||
["decimal", "Decimal"] => Constructor::Decimal,
|
||||
["fractions", "Fraction"] => Constructor::Fraction,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// `Decimal.from_decimal` doesn't exist.
|
||||
if matches!(
|
||||
(method_name, constructor),
|
||||
|
@ -131,14 +130,6 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
|||
break 'short_circuit;
|
||||
};
|
||||
|
||||
// Must be a call to the `float` builtin.
|
||||
let Some(func_name) = func.as_name_expr() else {
|
||||
break 'short_circuit;
|
||||
};
|
||||
if func_name.id != "float" {
|
||||
break 'short_circuit;
|
||||
};
|
||||
|
||||
// Must have exactly one argument, which is a string literal.
|
||||
if arguments.keywords.len() != 0 {
|
||||
break 'short_circuit;
|
||||
|
@ -156,7 +147,8 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
|||
break 'short_circuit;
|
||||
}
|
||||
|
||||
if !checker.semantic().is_builtin("float") {
|
||||
// Must be a call to the `float` builtin.
|
||||
if !semantic.match_builtin_expr(func, "float") {
|
||||
break 'short_circuit;
|
||||
};
|
||||
|
||||
|
|
|
@ -116,10 +116,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp
|
|||
func, arguments, ..
|
||||
}) => {
|
||||
// Must be a call to the `float` builtin.
|
||||
let Some(func_name) = func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if func_name.id != "float" {
|
||||
if !checker.semantic().match_builtin_expr(func, "float") {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -140,10 +137,6 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp
|
|||
return;
|
||||
}
|
||||
|
||||
if !checker.semantic().is_builtin("float") {
|
||||
return;
|
||||
};
|
||||
|
||||
let replacement = checker.locator().slice(float).to_string();
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
VerboseDecimalConstructor {
|
||||
|
|
|
@ -54,7 +54,7 @@ impl Violation for WriteWholeFile {
|
|||
/// FURB103
|
||||
pub(crate) fn write_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
||||
// `async` check here is more of a precaution.
|
||||
if with.is_async || !checker.semantic().is_builtin("open") {
|
||||
if with.is_async {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -92,3 +92,19 @@ FURB103.py:58:6: FURB103 `open` and `write` should be replaced by `Path("file.tx
|
|||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB103
|
||||
59 | f.write(foobar)
|
||||
|
|
||||
|
||||
FURB103.py:66:6: FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
|
|
||||
65 | # FURB103
|
||||
66 | with builtins.open("file.txt", "w", newline="\r\n") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB103
|
||||
67 | f.write(foobar)
|
||||
|
|
||||
|
||||
FURB103.py:74:6: FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
|
|
||||
73 | # FURB103
|
||||
74 | with o("file.txt", "w", newline="\r\n") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB103
|
||||
75 | f.write(foobar)
|
||||
|
|
||||
|
|
|
@ -204,4 +204,40 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
|||
40 40 | for _line in bar.readlines():
|
||||
41 41 | pass
|
||||
|
||||
FURB129.py:48:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
47 | with builtins.open("FURB129.py") as f:
|
||||
48 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
49 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
45 45 |
|
||||
46 46 |
|
||||
47 47 | with builtins.open("FURB129.py") as f:
|
||||
48 |- for line in f.readlines():
|
||||
48 |+ for line in f:
|
||||
49 49 | pass
|
||||
50 50 |
|
||||
51 51 |
|
||||
|
||||
FURB129.py:56:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
||||
|
|
||||
55 | with o("FURB129.py") as f:
|
||||
56 | for line in f.readlines():
|
||||
| ^^^^^^^^^^^^^ FURB129
|
||||
57 | pass
|
||||
|
|
||||
= help: Remove `readlines()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
53 53 |
|
||||
54 54 |
|
||||
55 55 | with o("FURB129.py") as f:
|
||||
56 |- for line in f.readlines():
|
||||
56 |+ for line in f:
|
||||
57 57 | pass
|
||||
58 58 |
|
||||
59 59 |
|
||||
|
|
|
@ -73,13 +73,8 @@ pub(crate) fn mutable_fromkeys_value(checker: &mut Checker, call: &ast::ExprCall
|
|||
if attr != "fromkeys" {
|
||||
return;
|
||||
}
|
||||
let Some(name_expr) = value.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
if name_expr.id != "dict" {
|
||||
return;
|
||||
}
|
||||
if !checker.semantic().is_builtin("dict") {
|
||||
let semantic = checker.semantic();
|
||||
if !semantic.match_builtin_expr(value, "dict") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -87,7 +82,7 @@ pub(crate) fn mutable_fromkeys_value(checker: &mut Checker, call: &ast::ExprCall
|
|||
let [keys, value] = &*call.arguments.args else {
|
||||
return;
|
||||
};
|
||||
if !is_mutable_expr(value, checker.semantic()) {
|
||||
if !is_mutable_expr(value, semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ RUF024.py:12:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromke
|
|||
12 |+{key: set() for key in pierogi_fillings}
|
||||
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
15 15 |
|
||||
15 15 | import builtins
|
||||
|
||||
RUF024.py:13:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys`
|
||||
|
|
||||
|
@ -91,6 +91,7 @@ RUF024.py:13:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromke
|
|||
13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
||||
14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
15 | import builtins
|
||||
|
|
||||
= help: Replace with comprehension
|
||||
|
||||
|
@ -101,8 +102,8 @@ RUF024.py:13:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromke
|
|||
13 |-dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
13 |+{key: {"pre": "populated!"} for key in pierogi_fillings}
|
||||
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
15 15 |
|
||||
16 16 | # Okay.
|
||||
15 15 | import builtins
|
||||
16 16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
|
||||
RUF024.py:14:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys`
|
||||
|
|
||||
|
@ -110,8 +111,8 @@ RUF024.py:14:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromke
|
|||
13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
||||
15 |
|
||||
16 | # Okay.
|
||||
15 | import builtins
|
||||
16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
|
|
||||
= help: Replace with comprehension
|
||||
|
||||
|
@ -121,8 +122,27 @@ RUF024.py:14:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromke
|
|||
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
14 |-dict.fromkeys(pierogi_fillings, dict())
|
||||
14 |+{key: dict() for key in pierogi_fillings}
|
||||
15 15 |
|
||||
16 16 | # Okay.
|
||||
17 17 | dict.fromkeys(pierogi_fillings)
|
||||
15 15 | import builtins
|
||||
16 16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
17 17 |
|
||||
|
||||
RUF024.py:16:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys`
|
||||
|
|
||||
14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
15 | import builtins
|
||||
16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
||||
17 |
|
||||
18 | # Okay.
|
||||
|
|
||||
= help: Replace with comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
||||
15 15 | import builtins
|
||||
16 |-builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||
16 |+{key: dict() for key in pierogi_fillings}
|
||||
17 17 |
|
||||
18 18 | # Okay.
|
||||
19 19 | dict.fromkeys(pierogi_fillings)
|
||||
|
|
|
@ -72,10 +72,7 @@ pub(crate) fn raise_vanilla_args(checker: &mut Checker, expr: &Expr) {
|
|||
// `NotImplementedError`.
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["", "NotImplementedError"])
|
||||
})
|
||||
.match_builtin_expr(func, "NotImplementedError")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -63,15 +63,12 @@ impl Violation for RaiseVanillaClass {
|
|||
|
||||
/// TRY002
|
||||
pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
let node = if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||
func
|
||||
} else {
|
||||
expr
|
||||
})
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "Exception"]))
|
||||
{
|
||||
};
|
||||
if checker.semantic().match_builtin_expr(node, "Exception") {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(RaiseVanillaClass, expr.range()));
|
||||
|
|
|
@ -111,13 +111,8 @@ pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: &
|
|||
|| handled_exceptions.iter().any(|expr| {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(expr)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "Exception" | "BaseException"]
|
||||
)
|
||||
})
|
||||
.resolve_builtin_symbol(expr)
|
||||
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||
})
|
||||
{
|
||||
checker
|
||||
|
|
|
@ -74,26 +74,20 @@ fn has_control_flow(stmt: &Stmt) -> bool {
|
|||
}
|
||||
|
||||
/// Returns `true` if an [`Expr`] is a call to check types.
|
||||
fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool {
|
||||
checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "isinstance" | "issubclass" | "callable"]
|
||||
)
|
||||
})
|
||||
fn check_type_check_call(semantic: &SemanticModel, call: &Expr) -> bool {
|
||||
semantic
|
||||
.resolve_builtin_symbol(call)
|
||||
.is_some_and(|builtin| matches!(builtin, "isinstance" | "issubclass" | "callable"))
|
||||
}
|
||||
|
||||
/// Returns `true` if an [`Expr`] is a test to check types (e.g. via isinstance)
|
||||
fn check_type_check_test(checker: &mut Checker, test: &Expr) -> bool {
|
||||
fn check_type_check_test(semantic: &SemanticModel, test: &Expr) -> bool {
|
||||
match test {
|
||||
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values
|
||||
.iter()
|
||||
.all(|expr| check_type_check_test(checker, expr)),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => check_type_check_test(checker, operand),
|
||||
Expr::Call(ast::ExprCall { func, .. }) => check_type_check_call(checker, func),
|
||||
.all(|expr| check_type_check_test(semantic, expr)),
|
||||
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => check_type_check_test(semantic, operand),
|
||||
Expr::Call(ast::ExprCall { func, .. }) => check_type_check_call(semantic, func),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -161,14 +155,15 @@ pub(crate) fn type_check_without_type_error(
|
|||
elif_else_clauses,
|
||||
..
|
||||
} = stmt_if;
|
||||
|
||||
if let Some(Stmt::If(ast::StmtIf { test, .. })) = parent {
|
||||
if !check_type_check_test(checker, test) {
|
||||
if !check_type_check_test(checker.semantic(), test) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider the body when the `if` condition is all type-related
|
||||
if !check_type_check_test(checker, test) {
|
||||
if !check_type_check_test(checker.semantic(), test) {
|
||||
return;
|
||||
}
|
||||
check_body(checker, body);
|
||||
|
@ -176,7 +171,7 @@ pub(crate) fn type_check_without_type_error(
|
|||
for clause in elif_else_clauses {
|
||||
if let Some(test) = &clause.test {
|
||||
// If there are any `elif`, they must all also be type-related
|
||||
if !check_type_check_test(checker, test) {
|
||||
if !check_type_check_test(checker.semantic(), test) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,9 +47,15 @@ impl<'a> QualifiedName<'a> {
|
|||
self.0.as_slice()
|
||||
}
|
||||
|
||||
/// If the first segment is empty, the `CallPath` is that of a builtin.
|
||||
/// If the first segment is empty, the `CallPath` represents a "builtin binding".
|
||||
///
|
||||
/// A builtin binding is the binding that a symbol has if it was part of Python's
|
||||
/// global scope without any imports taking place. However, if builtin members are
|
||||
/// accessed explicitly via the `builtins` module, they will not have a
|
||||
/// "builtin binding", so this method will return `false`.
|
||||
///
|
||||
/// Ex) `["", "bool"]` -> `"bool"`
|
||||
pub fn is_builtin(&self) -> bool {
|
||||
fn is_builtin(&self) -> bool {
|
||||
matches!(self.segments(), ["", ..])
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ pub fn classify(
|
|||
semantic
|
||||
.resolve_qualified_name(map_callable(expr))
|
||||
.is_some_and( |qualified_name| {
|
||||
matches!(qualified_name.segments(), ["", "type"] | ["abc", "ABCMeta"])
|
||||
matches!(qualified_name.segments(), ["" | "builtins", "type"] | ["abc", "ABCMeta"])
|
||||
})
|
||||
})
|
||||
|| decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators))
|
||||
|
@ -63,7 +63,7 @@ fn is_static_method(
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
||||
["" | "builtins", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
||||
) || staticmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
||||
|
@ -103,7 +103,7 @@ fn is_class_method(
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "classmethod"] | ["abc", "abstractclassmethod"]
|
||||
["" | "builtins", "classmethod"] | ["abc", "abstractclassmethod"]
|
||||
) || classmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
||||
|
|
|
@ -686,7 +686,7 @@ impl TypeChecker for IoBaseChecker {
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["io", "open" | "open_code"] | ["os" | "", "open"]
|
||||
["io", "open" | "open_code"] | ["os" | "" | "builtins", "open"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,20 +15,16 @@ pub enum Visibility {
|
|||
|
||||
/// Returns `true` if a function is a "static method".
|
||||
pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
||||
decorator_list.iter().any(|decorator| {
|
||||
semantic
|
||||
.resolve_qualified_name(map_callable(&decorator.expression))
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "staticmethod"]))
|
||||
})
|
||||
decorator_list
|
||||
.iter()
|
||||
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "staticmethod"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a "class method".
|
||||
pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
||||
decorator_list.iter().any(|decorator| {
|
||||
semantic
|
||||
.resolve_qualified_name(map_callable(&decorator.expression))
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "classmethod"]))
|
||||
})
|
||||
decorator_list
|
||||
.iter()
|
||||
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "classmethod"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is an `@overload`.
|
||||
|
@ -79,7 +75,7 @@ pub fn is_property(
|
|||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["", "property"] | ["functools", "cached_property"]
|
||||
["" | "builtins", "property"] | ["functools", "cached_property"]
|
||||
) || extra_properties
|
||||
.iter()
|
||||
.any(|extra_property| extra_property.segments() == qualified_name.segments())
|
||||
|
|
|
@ -251,12 +251,53 @@ impl<'a> SemanticModel<'a> {
|
|||
}
|
||||
|
||||
/// Return `true` if `member` is bound as a builtin.
|
||||
///
|
||||
/// Note that a "builtin binding" does *not* include explicit lookups via the `builtins`
|
||||
/// module, e.g. `import builtins; builtins.open`. It *only* includes the bindings
|
||||
/// that are pre-populated in Python's global scope before any imports have taken place.
|
||||
pub fn is_builtin(&self, member: &str) -> bool {
|
||||
self.lookup_symbol(member)
|
||||
.map(|binding_id| &self.bindings[binding_id])
|
||||
.is_some_and(|binding| binding.kind.is_builtin())
|
||||
}
|
||||
|
||||
/// If `expr` is a reference to a builtins symbol,
|
||||
/// return the name of that symbol. Else, return `None`.
|
||||
///
|
||||
/// This method returns `true` both for "builtin bindings"
|
||||
/// (present even without any imports, e.g. `open()`), and for explicit lookups
|
||||
/// via the `builtins` module (e.g. `import builtins; builtins.open()`).
|
||||
pub fn resolve_builtin_symbol<'expr>(&'a self, expr: &'expr Expr) -> Option<&'a str>
|
||||
where
|
||||
'expr: 'a,
|
||||
{
|
||||
// Fast path: we only need to worry about name expressions
|
||||
if !self.seen_module(Modules::BUILTINS) {
|
||||
let name = &expr.as_name_expr()?.id;
|
||||
return if self.is_builtin(name) {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
// Slow path: we have to consider names and attributes
|
||||
let qualified_name = self.resolve_qualified_name(expr)?;
|
||||
match qualified_name.segments() {
|
||||
["" | "builtins", name] => Some(*name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if `expr` is a reference to `builtins.$target`,
|
||||
/// i.e. either `object` (where `object` is not overridden in the global scope),
|
||||
/// or `builtins.object` (where `builtins` is imported as a module at the top level)
|
||||
pub fn match_builtin_expr(&self, expr: &Expr, symbol: &str) -> bool {
|
||||
debug_assert!(!symbol.contains('.'));
|
||||
self.resolve_builtin_symbol(expr)
|
||||
.is_some_and(|name| name == symbol)
|
||||
}
|
||||
|
||||
/// Return `true` if `member` is an "available" symbol, i.e., a symbol that has not been bound
|
||||
/// in the current scope, or in any containing scope.
|
||||
pub fn is_available(&self, member: &str) -> bool {
|
||||
|
@ -1138,6 +1179,7 @@ impl<'a> SemanticModel<'a> {
|
|||
pub fn add_module(&mut self, module: &str) {
|
||||
match module {
|
||||
"_typeshed" => self.seen.insert(Modules::TYPESHED),
|
||||
"builtins" => self.seen.insert(Modules::BUILTINS),
|
||||
"collections" => self.seen.insert(Modules::COLLECTIONS),
|
||||
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
|
||||
"datetime" => self.seen.insert(Modules::DATETIME),
|
||||
|
@ -1708,6 +1750,7 @@ bitflags! {
|
|||
const TYPING_EXTENSIONS = 1 << 15;
|
||||
const TYPESHED = 1 << 16;
|
||||
const DATACLASSES = 1 << 17;
|
||||
const BUILTINS = 1 << 18;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
pub fn is_standard_library_generic(qualified_name: &[&str]) -> bool {
|
||||
matches!(
|
||||
qualified_name,
|
||||
["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"]
|
||||
| [
|
||||
[
|
||||
"" | "builtins",
|
||||
"dict" | "frozenset" | "list" | "set" | "tuple" | "type"
|
||||
] | [
|
||||
"collections" | "typing" | "typing_extensions",
|
||||
"ChainMap" | "Counter"
|
||||
]
|
||||
| ["collections" | "typing", "OrderedDict"]
|
||||
] | ["collections" | "typing", "OrderedDict"]
|
||||
| ["collections", "defaultdict" | "deque"]
|
||||
| [
|
||||
"collections",
|
||||
|
@ -247,7 +248,7 @@ pub fn is_immutable_non_generic_type(qualified_name: &[&str]) -> bool {
|
|||
pub fn is_immutable_generic_type(qualified_name: &[&str]) -> bool {
|
||||
matches!(
|
||||
qualified_name,
|
||||
["", "tuple"]
|
||||
["" | "builtins", "tuple"]
|
||||
| [
|
||||
"collections",
|
||||
"abc",
|
||||
|
@ -285,7 +286,7 @@ pub fn is_immutable_generic_type(qualified_name: &[&str]) -> bool {
|
|||
pub fn is_mutable_return_type(qualified_name: &[&str]) -> bool {
|
||||
matches!(
|
||||
qualified_name,
|
||||
["", "dict" | "list" | "set"]
|
||||
["" | "builtins", "dict" | "list" | "set"]
|
||||
| [
|
||||
"collections",
|
||||
"Counter" | "OrderedDict" | "defaultdict" | "deque"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue