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?")
|
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():
|
def this_is_fine():
|
||||||
o = object()
|
o = object()
|
||||||
if callable(o):
|
if callable(o):
|
||||||
|
|
|
@ -64,3 +64,6 @@ setattr(*foo, "bar", None)
|
||||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739800901
|
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739800901
|
||||||
getattr(self.
|
getattr(self.
|
||||||
registration.registry, '__name__')
|
registration.registry, '__name__')
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
builtins.getattr(foo, "bar")
|
||||||
|
|
|
@ -23,3 +23,7 @@ zip([1, 2, 3], repeat(1, times=None))
|
||||||
# Errors (limited iterators).
|
# Errors (limited iterators).
|
||||||
zip([1, 2, 3], repeat(1, 1))
|
zip([1, 2, 3], repeat(1, 1))
|
||||||
zip([1, 2, 3], repeat(1, times=4))
|
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
|
# PIE808
|
||||||
range(0, 10)
|
range(0, 10)
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
builtins.range(0, 10)
|
||||||
|
|
||||||
# OK
|
# OK
|
||||||
range(x, 10)
|
range(x, 10)
|
||||||
range(-15, 10)
|
range(-15, 10)
|
||||||
|
|
|
@ -73,3 +73,10 @@ class BadFive:
|
||||||
class BadSix:
|
class BadSix:
|
||||||
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
|
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
|
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
|
class Good(Tuple[str, int, float]): # OK
|
||||||
__slots__ = ("foo",)
|
__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
|
for i in list(foo_list): # OK
|
||||||
if True:
|
if True:
|
||||||
del foo_list[i + 1]
|
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
|
#: E721
|
||||||
dtype == float
|
dtype == float
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
if builtins.type(res) == memoryview: # E721
|
||||||
|
pass
|
||||||
|
|
|
@ -4,3 +4,8 @@ def f() -> None:
|
||||||
|
|
||||||
def g() -> None:
|
def g() -> None:
|
||||||
raise NotImplemented
|
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("rwx") # [bad-open-mode]
|
||||||
pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
|
pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
|
||||||
pathlib.Path(NAME).open("rwx", encoding="utf-8") # [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:
|
if y == npy_nan:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
# PLW0117
|
||||||
|
if x == builtins.float("nan"):
|
||||||
|
pass
|
||||||
|
|
||||||
# OK
|
# OK
|
||||||
if math.isnan(x):
|
if math.isnan(x):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -39,3 +39,6 @@ max(max(tuples_list))
|
||||||
|
|
||||||
# Starred argument should be copied as it is.
|
# Starred argument should be copied as it is.
|
||||||
max(1, max(*a))
|
max(1, max(*a))
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
builtins.min(1, min(2, 3))
|
||||||
|
|
|
@ -152,3 +152,9 @@ object = A
|
||||||
|
|
||||||
class B(object):
|
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)
|
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.
|
# Non-errors.
|
||||||
|
|
||||||
with open("file.txt", errors="ignore", mode="wb") as f:
|
with open("file.txt", errors="ignore", mode="wb") as f:
|
||||||
|
|
|
@ -41,6 +41,22 @@ def func():
|
||||||
pass
|
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
|
# False positives
|
||||||
def func(f):
|
def func(f):
|
||||||
for _line in f.readlines():
|
for _line in f.readlines():
|
||||||
|
|
|
@ -12,6 +12,8 @@ dict.fromkeys(pierogi_fillings, {})
|
||||||
dict.fromkeys(pierogi_fillings, set())
|
dict.fromkeys(pierogi_fillings, set())
|
||||||
dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
dict.fromkeys(pierogi_fillings, dict())
|
dict.fromkeys(pierogi_fillings, dict())
|
||||||
|
import builtins
|
||||||
|
builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||||
|
|
||||||
# Okay.
|
# Okay.
|
||||||
dict.fromkeys(pierogi_fillings)
|
dict.fromkeys(pierogi_fillings)
|
||||||
|
|
|
@ -784,17 +784,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
}) => {
|
}) => {
|
||||||
let mut handled_exceptions = Exceptions::empty();
|
let mut handled_exceptions = Exceptions::empty();
|
||||||
for type_ in extract_handled_exceptions(handlers) {
|
for type_ in extract_handled_exceptions(handlers) {
|
||||||
if let Some(qualified_name) = self.semantic.resolve_qualified_name(type_) {
|
if let Some(builtins_name) = self.semantic.resolve_builtin_symbol(type_) {
|
||||||
match qualified_name.segments() {
|
match builtins_name {
|
||||||
["", "NameError"] => {
|
"NameError" => handled_exceptions |= Exceptions::NAME_ERROR,
|
||||||
handled_exceptions |= Exceptions::NAME_ERROR;
|
"ModuleNotFoundError" => {
|
||||||
}
|
|
||||||
["", "ModuleNotFoundError"] => {
|
|
||||||
handled_exceptions |= Exceptions::MODULE_NOT_FOUND_ERROR;
|
handled_exceptions |= Exceptions::MODULE_NOT_FOUND_ERROR;
|
||||||
}
|
}
|
||||||
["", "ImportError"] => {
|
"ImportError" => handled_exceptions |= Exceptions::IMPORT_ERROR,
|
||||||
handled_exceptions |= Exceptions::IMPORT_ERROR;
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1125,7 +1121,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
Some(typing::Callable::MypyExtension)
|
Some(typing::Callable::MypyExtension)
|
||||||
} else if matches!(qualified_name.segments(), ["", "bool"]) {
|
} else if matches!(qualified_name.segments(), ["" | "builtins", "bool"])
|
||||||
|
{
|
||||||
Some(typing::Callable::Bool)
|
Some(typing::Callable::Bool)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -229,6 +229,31 @@ impl<'a> Importer<'a> {
|
||||||
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
|
.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`].
|
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
|
||||||
fn find_symbol(
|
fn find_symbol(
|
||||||
symbol: &ImportRequest,
|
symbol: &ImportRequest,
|
||||||
|
|
|
@ -63,7 +63,7 @@ fn is_open_sleep_or_subprocess_call(func: &Expr, semantic: &SemanticModel) -> bo
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["", "open"]
|
["" | "builtins", "open"]
|
||||||
| ["time", "sleep"]
|
| ["time", "sleep"]
|
||||||
| [
|
| [
|
||||||
"subprocess",
|
"subprocess",
|
||||||
|
|
|
@ -24,23 +24,13 @@ pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticMode
|
||||||
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ {
|
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ {
|
||||||
elts.iter().any(|type_| {
|
elts.iter().any(|type_| {
|
||||||
semantic
|
semantic
|
||||||
.resolve_qualified_name(type_)
|
.resolve_builtin_symbol(type_)
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||||
matches!(
|
|
||||||
qualified_name.segments(),
|
|
||||||
["", "Exception" | "BaseException"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
semantic
|
semantic
|
||||||
.resolve_qualified_name(type_)
|
.resolve_builtin_symbol(type_)
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||||
matches!(
|
|
||||||
qualified_name.segments(),
|
|
||||||
["", "Exception" | "BaseException"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,24 +95,22 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(exception) =
|
let semantic = checker.semantic();
|
||||||
checker
|
|
||||||
.semantic()
|
let Some(builtin_symbol) = semantic.resolve_builtin_symbol(arg) else {
|
||||||
.resolve_qualified_name(arg)
|
|
||||||
.and_then(|qualified_name| match qualified_name.segments() {
|
|
||||||
["", "Exception"] => Some(ExceptionKind::Exception),
|
|
||||||
["", "BaseException"] => Some(ExceptionKind::BaseException),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
else {
|
|
||||||
return;
|
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")
|
let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
|
||||||
{
|
{
|
||||||
AssertionKind::AssertRaises
|
AssertionKind::AssertRaises
|
||||||
} else if checker
|
} else if semantic
|
||||||
.semantic()
|
|
||||||
.resolve_qualified_name(func)
|
.resolve_qualified_name(func)
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]))
|
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"]))
|
||||||
&& arguments.find_keyword("match").is_none()
|
&& arguments.find_keyword("match").is_none()
|
||||||
|
|
|
@ -54,12 +54,6 @@ pub(crate) fn getattr_with_constant(
|
||||||
func: &Expr,
|
func: &Expr,
|
||||||
args: &[Expr],
|
args: &[Expr],
|
||||||
) {
|
) {
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if id != "getattr" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let [obj, arg] = args else {
|
let [obj, arg] = args else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -75,7 +69,7 @@ pub(crate) fn getattr_with_constant(
|
||||||
if is_mangled_private(value.to_str()) {
|
if is_mangled_private(value.to_str()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !checker.semantic().is_builtin("getattr") {
|
if !checker.semantic().match_builtin_expr(func, "getattr") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,12 +68,6 @@ pub(crate) fn setattr_with_constant(
|
||||||
func: &Expr,
|
func: &Expr,
|
||||||
args: &[Expr],
|
args: &[Expr],
|
||||||
) {
|
) {
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if id != "setattr" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let [obj, name, value] = args else {
|
let [obj, name, value] = args else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -89,7 +83,7 @@ pub(crate) fn setattr_with_constant(
|
||||||
if is_mangled_private(name.to_str()) {
|
if is_mangled_private(name.to_str()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !checker.semantic().is_builtin("setattr") {
|
if !checker.semantic().match_builtin_expr(func, "setattr") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use ruff_python_ast::{self as ast, Expr};
|
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
@ -58,12 +57,6 @@ pub(crate) fn unreliable_callable_check(
|
||||||
func: &Expr,
|
func: &Expr,
|
||||||
args: &[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 {
|
let [obj, attr, ..] = args else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -73,15 +66,27 @@ pub(crate) fn unreliable_callable_check(
|
||||||
if value != "__call__" {
|
if value != "__call__" {
|
||||||
return;
|
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());
|
let mut diagnostic = Diagnostic::new(UnreliableCallableCheck, expr.range());
|
||||||
if id == "hasattr" {
|
if builtins_function == "hasattr" {
|
||||||
if checker.semantic().is_builtin("callable") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
format!("callable({})", checker.locator().slice(obj)),
|
"callable",
|
||||||
|
expr.start(),
|
||||||
|
checker.semantic(),
|
||||||
|
)?;
|
||||||
|
let binding_edit = Edit::range_replacement(
|
||||||
|
format!("{binding}({})", checker.locator().slice(obj)),
|
||||||
expr.range(),
|
expr.range(),
|
||||||
)));
|
);
|
||||||
}
|
Ok(Fix::safe_edits(binding_edit, import_edit))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,18 +52,18 @@ impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
|
||||||
|
|
||||||
/// B905
|
/// B905
|
||||||
pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::ExprCall) {
|
pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::ExprCall) {
|
||||||
if let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() {
|
let semantic = checker.semantic();
|
||||||
if id == "zip"
|
|
||||||
&& checker.semantic().is_builtin("zip")
|
if semantic.match_builtin_expr(&call.func, "zip")
|
||||||
&& call.arguments.find_keyword("strict").is_none()
|
&& call.arguments.find_keyword("strict").is_none()
|
||||||
&& !call
|
&& !call
|
||||||
.arguments
|
.arguments
|
||||||
.args
|
.args
|
||||||
.iter()
|
.iter()
|
||||||
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
|
.any(|arg| is_infinite_iterator(arg, semantic))
|
||||||
{
|
{
|
||||||
let mut diagnostic = Diagnostic::new(ZipWithoutExplicitStrict, call.range());
|
checker.diagnostics.push(
|
||||||
diagnostic.set_fix(Fix::applicable_edit(
|
Diagnostic::new(ZipWithoutExplicitStrict, call.range()).with_fix(Fix::applicable_edit(
|
||||||
add_argument(
|
add_argument(
|
||||||
"strict=False",
|
"strict=False",
|
||||||
&call.arguments,
|
&call.arguments,
|
||||||
|
@ -81,9 +81,8 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
||||||
} else {
|
} else {
|
||||||
Applicability::Safe
|
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()`
|
= 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.
|
65 | / getattr(self.
|
||||||
66 | | registration.registry, '__name__')
|
66 | | registration.registry, '__name__')
|
||||||
| |_____________________________________^ B009
|
| |_____________________________________^ B009
|
||||||
|
67 |
|
||||||
|
68 | import builtins
|
||||||
|
|
|
|
||||||
= help: Replace `getattr` with attribute access
|
= 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__')
|
66 |- registration.registry, '__name__')
|
||||||
65 |+(self.
|
65 |+(self.
|
||||||
66 |+ registration.registry).__name__
|
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))
|
||||||
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
|
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
|
||||||
25 25 | zip([1, 2, 3], repeat(1, times=4))
|
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
|
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))
|
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))
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||||
|
26 |
|
||||||
|
27 | import builtins
|
||||||
|
|
|
|
||||||
= help: Add explicit `strict=False`
|
= 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))
|
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))
|
||||||
25 |+zip([1, 2, 3], repeat(1, times=4), strict=False)
|
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 {
|
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
semantic
|
semantic.match_builtin_expr(func, "locals")
|
||||||
.resolve_qualified_name(func)
|
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "locals"]))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,11 +108,7 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) {
|
||||||
arguments: Arguments { keywords, .. },
|
arguments: Arguments { keywords, .. },
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
if checker
|
if checker.semantic().match_builtin_expr(func, "dict") {
|
||||||
.semantic()
|
|
||||||
.resolve_qualified_name(func)
|
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "dict"]))
|
|
||||||
{
|
|
||||||
for keyword in keywords.iter() {
|
for keyword in keywords.iter() {
|
||||||
if let Some(attr) = &keyword.arg {
|
if let Some(attr) = &keyword.arg {
|
||||||
if is_reserved_attr(attr) {
|
if is_reserved_attr(attr) {
|
||||||
|
|
|
@ -43,17 +43,6 @@ impl AlwaysFixableViolation for UnnecessaryRangeStart {
|
||||||
|
|
||||||
/// PIE808
|
/// PIE808
|
||||||
pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCall) {
|
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.
|
// `range` doesn't accept keyword arguments.
|
||||||
if !call.arguments.keywords.is_empty() {
|
if !call.arguments.keywords.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
@ -76,6 +65,11 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
|
||||||
return;
|
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());
|
let mut diagnostic = Diagnostic::new(UnnecessaryRangeStart, start.range());
|
||||||
diagnostic.try_set_fix(|| {
|
diagnostic.try_set_fix(|| {
|
||||||
remove_argument(
|
remove_argument(
|
||||||
|
|
|
@ -7,7 +7,7 @@ PIE808.py:2:7: PIE808 [*] Unnecessary `start` argument in `range`
|
||||||
2 | range(0, 10)
|
2 | range(0, 10)
|
||||||
| ^ PIE808
|
| ^ PIE808
|
||||||
3 |
|
3 |
|
||||||
4 | # OK
|
4 | import builtins
|
||||||
|
|
|
|
||||||
= help: Remove `start` argument
|
= help: Remove `start` argument
|
||||||
|
|
||||||
|
@ -16,7 +16,25 @@ PIE808.py:2:7: PIE808 [*] Unnecessary `start` argument in `range`
|
||||||
2 |-range(0, 10)
|
2 |-range(0, 10)
|
||||||
2 |+range(10)
|
2 |+range(10)
|
||||||
3 3 |
|
3 3 |
|
||||||
4 4 | # OK
|
4 4 | import builtins
|
||||||
5 5 | range(x, 10)
|
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
|
/// T201, T203
|
||||||
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||||
let mut diagnostic = {
|
let semantic = checker.semantic();
|
||||||
let qualified_name = checker.semantic().resolve_qualified_name(&call.func);
|
|
||||||
if qualified_name
|
let Some(qualified_name) = semantic.resolve_qualified_name(&call.func) else {
|
||||||
.as_ref()
|
return;
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"]))
|
};
|
||||||
{
|
|
||||||
|
let mut diagnostic = match qualified_name.segments() {
|
||||||
|
["" | "builtins", "print"] => {
|
||||||
// If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`,
|
// If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`,
|
||||||
// or `"sys.stderr"`), don't trigger T201.
|
// or `"sys.stderr"`), don't trigger T201.
|
||||||
if let Some(keyword) = call.arguments.find_keyword("file") {
|
if let Some(keyword) = call.arguments.find_keyword("file") {
|
||||||
if !keyword.value.is_none_literal_expr() {
|
if !keyword.value.is_none_literal_expr() {
|
||||||
if checker
|
if semantic.resolve_qualified_name(&keyword.value).map_or(
|
||||||
.semantic()
|
true,
|
||||||
.resolve_qualified_name(&keyword.value)
|
|qualified_name| {
|
||||||
.map_or(true, |qualified_name| {
|
!matches!(qualified_name.segments(), ["sys", "stdout" | "stderr"])
|
||||||
qualified_name.segments() != ["sys", "stdout"]
|
},
|
||||||
&& qualified_name.segments() != ["sys", "stderr"]
|
) {
|
||||||
})
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Diagnostic::new(Print, call.func.range())
|
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()) {
|
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.
|
// Remove the `print`, if it's a standalone statement.
|
||||||
if checker.semantic().current_expression_parent().is_none() {
|
if semantic.current_expression_parent().is_none() {
|
||||||
let statement = checker.semantic().current_statement();
|
let statement = semantic.current_statement();
|
||||||
let parent = checker.semantic().current_statement_parent();
|
let parent = semantic.current_statement_parent();
|
||||||
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
|
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
|
||||||
diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation(
|
diagnostic.set_fix(
|
||||||
checker.semantic().current_statement_parent_id(),
|
Fix::unsafe_edit(edit)
|
||||||
)));
|
.isolate(Checker::isolation(semantic.current_statement_parent_id())),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
|
|
|
@ -72,11 +72,13 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !checker.semantic().current_scope().kind.is_class() {
|
let semantic = checker.semantic();
|
||||||
|
|
||||||
|
if !semantic.current_scope().kind.is_class() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker.semantic().match_typing_expr(annotation, "Any") {
|
if semantic.match_typing_expr(annotation, "Any") {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
AnyEqNeAnnotation {
|
AnyEqNeAnnotation {
|
||||||
method_name: name.to_string(),
|
method_name: name.to_string(),
|
||||||
|
@ -84,12 +86,15 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
|
||||||
annotation.range(),
|
annotation.range(),
|
||||||
);
|
);
|
||||||
// Ex) `def __eq__(self, obj: Any): ...`
|
// Ex) `def __eq__(self, obj: Any): ...`
|
||||||
if checker.semantic().is_builtin("object") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"object".to_string(),
|
"object",
|
||||||
annotation.range(),
|
annotation.start(),
|
||||||
)));
|
semantic,
|
||||||
}
|
)?;
|
||||||
|
let binding_edit = Edit::range_replacement(binding, annotation.range());
|
||||||
|
Ok(Fix::safe_edits(binding_edit, import_edit))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,12 +181,15 @@ fn check_short_args_list(checker: &mut Checker, parameters: &Parameters, func_ki
|
||||||
annotation.range(),
|
annotation.range(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if checker.semantic().is_builtin("object") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"object".to_string(),
|
"object",
|
||||||
annotation.range(),
|
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);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
|
@ -213,7 +216,9 @@ fn check_positional_args(
|
||||||
|
|
||||||
let validations: [(ErrorKind, AnnotationValidator); 3] = [
|
let validations: [(ErrorKind, AnnotationValidator); 3] = [
|
||||||
(ErrorKind::FirstArgBadAnnotation, is_base_exception_type),
|
(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),
|
(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.
|
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
|
||||||
fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
semantic
|
semantic
|
||||||
|
@ -351,15 +343,8 @@ fn is_base_exception_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if semantic.match_typing_expr(value, "Type")
|
if semantic.match_typing_expr(value, "Type") || semantic.match_builtin_expr(value, "type") {
|
||||||
|| semantic
|
semantic.match_builtin_expr(slice, "BaseException")
|
||||||
.resolve_qualified_name(value)
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|qualified_name| {
|
|
||||||
matches!(qualified_name.segments(), ["" | "builtins", "type"])
|
|
||||||
})
|
|
||||||
{
|
|
||||||
is_base_exception(slice, semantic)
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,20 +96,20 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
|
||||||
let mut has_complex = false;
|
let mut has_complex = false;
|
||||||
let mut has_int = false;
|
let mut has_int = false;
|
||||||
|
|
||||||
let mut func = |expr: &Expr, _parent: &Expr| {
|
let mut find_numeric_type = |expr: &Expr, _parent: &Expr| {
|
||||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) else {
|
let Some(builtin_type) = checker.semantic().resolve_builtin_symbol(expr) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match qualified_name.segments() {
|
match builtin_type {
|
||||||
["" | "builtins", "int"] => has_int = true,
|
"int" => has_int = true,
|
||||||
["" | "builtins", "float"] => has_float = true,
|
"float" => has_float = true,
|
||||||
["" | "builtins", "complex"] => has_complex = 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_complex {
|
||||||
if has_float {
|
if has_float {
|
||||||
|
|
|
@ -78,13 +78,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if checker
|
if !checker.semantic().match_builtin_expr(returns, "str") {
|
||||||
.semantic()
|
|
||||||
.resolve_qualified_name(returns)
|
|
||||||
.map_or(true, |qualified_name| {
|
|
||||||
!matches!(qualified_name.segments(), ["" | "builtins", "str"])
|
|
||||||
})
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,46 +53,34 @@ impl Violation for UnnecessaryTypeUnion {
|
||||||
|
|
||||||
/// PYI055
|
/// PYI055
|
||||||
pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr) {
|
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.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`
|
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`
|
||||||
let subscript = union.as_subscript_expr();
|
let subscript = union.as_subscript_expr();
|
||||||
if subscript.is_some_and(|subscript| {
|
if subscript.is_some_and(|subscript| !semantic.match_typing_expr(&subscript.value, "Union")) {
|
||||||
!checker
|
|
||||||
.semantic()
|
|
||||||
.match_typing_expr(&subscript.value, "Union")
|
|
||||||
}) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut type_exprs = Vec::new();
|
let mut type_exprs: Vec<&Expr> = Vec::new();
|
||||||
let mut other_exprs = Vec::new();
|
let mut other_exprs: Vec<&Expr> = Vec::new();
|
||||||
|
|
||||||
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| {
|
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| match expr {
|
||||||
let subscript = expr.as_subscript_expr();
|
Expr::Subscript(ast::ExprSubscript { slice, value, .. }) => {
|
||||||
|
if semantic.match_builtin_expr(value, "type") {
|
||||||
if subscript.is_none() {
|
type_exprs.push(slice);
|
||||||
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());
|
|
||||||
} else {
|
} else {
|
||||||
other_exprs.push(expr);
|
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 {
|
if type_exprs.len() > 1 {
|
||||||
let type_members: Vec<String> = type_exprs
|
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(),
|
union.range(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if checker.semantic().is_builtin("type") {
|
if semantic.is_builtin("type") {
|
||||||
let content = if let Some(subscript) = subscript {
|
let content = if let Some(subscript) = subscript {
|
||||||
let types = &Expr::Subscript(ast::ExprSubscript {
|
let types = &Expr::Subscript(ast::ExprSubscript {
|
||||||
value: Box::new(Expr::Name(ast::ExprName {
|
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.
|
/// If `call` is an `isinstance()` call, return its target.
|
||||||
fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> {
|
fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Expr> {
|
||||||
// Verify that this is an `isinstance` call.
|
// Verify that this is an `isinstance` call.
|
||||||
let Expr::Call(ast::ExprCall {
|
let ast::ExprCall {
|
||||||
func,
|
func,
|
||||||
arguments:
|
arguments:
|
||||||
Arguments {
|
Arguments {
|
||||||
|
@ -310,23 +310,14 @@ fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option<
|
||||||
range: _,
|
range: _,
|
||||||
},
|
},
|
||||||
range: _,
|
range: _,
|
||||||
}) = &call
|
} = call.as_call_expr()?;
|
||||||
else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !keywords.is_empty() {
|
if !keywords.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Expr::Name(ast::ExprName { id: func_name, .. }) = func.as_ref() else {
|
if !semantic.match_builtin_expr(func, "isinstance") {
|
||||||
return None;
|
|
||||||
};
|
|
||||||
if func_name != "isinstance" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if !semantic.is_builtin("isinstance") {
|
|
||||||
return None;
|
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`.
|
/// Return `true` if `func` is the builtin `open` or `pathlib.Path(...).open`.
|
||||||
fn is_open(checker: &mut Checker, func: &Expr) -> bool {
|
fn is_open(semantic: &SemanticModel, func: &Expr) -> bool {
|
||||||
match func {
|
// open(...)
|
||||||
// pathlib.Path(...).open()
|
if semantic.match_builtin_expr(func, "open") {
|
||||||
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
|
return true;
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// open(...)
|
|
||||||
Expr::Name(ast::ExprName { id, .. }) => {
|
|
||||||
id.as_str() == "open" && checker.semantic().is_builtin("open")
|
|
||||||
}
|
|
||||||
_ => 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.
|
/// Return `true` if the current expression is followed by a `close` call.
|
||||||
|
@ -161,27 +163,29 @@ fn is_closed(semantic: &SemanticModel) -> bool {
|
||||||
|
|
||||||
/// SIM115
|
/// SIM115
|
||||||
pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex) `open("foo.txt").close()`
|
// Ex) `open("foo.txt").close()`
|
||||||
if is_closed(checker.semantic()) {
|
if is_closed(semantic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex) `with open("foo.txt") as f: ...`
|
// Ex) `with open("foo.txt") as f: ...`
|
||||||
if checker.semantic().current_statement().is_with_stmt() {
|
if semantic.current_statement().is_with_stmt() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex) `with contextlib.ExitStack() as exit_stack: ...`
|
// Ex) `with contextlib.ExitStack() as exit_stack: ...`
|
||||||
if match_exit_stack(checker.semantic()) {
|
if match_exit_stack(semantic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex) `with contextlib.AsyncExitStack() as exit_stack: ...`
|
// Ex) `with contextlib.AsyncExitStack() as exit_stack: ...`
|
||||||
if match_async_exit_stack(checker.semantic()) {
|
if match_async_exit_stack(semantic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,16 +55,11 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
|
||||||
if bases.iter().any(|base| {
|
if bases.iter().any(|base| {
|
||||||
checker
|
let base = map_subscript(base);
|
||||||
.semantic()
|
semantic.match_builtin_expr(base, "tuple") || semantic.match_typing_expr(base, "Tuple")
|
||||||
.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")
|
|
||||||
})
|
|
||||||
}) {
|
}) {
|
||||||
if !has_slots(&class.body) {
|
if !has_slots(&class.body) {
|
||||||
checker
|
checker
|
||||||
|
|
|
@ -22,4 +22,11 @@ SLOT001.py:16:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
||||||
17 | pass
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
if !checker.semantic().match_builtin_expr(func, "list") {
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(id == "list" && checker.semantic().is_builtin("list")) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -221,4 +221,20 @@ PERF101.py:69:10: PERF101 [*] Do not cast an iterable to `list` before iterating
|
||||||
71 71 |
|
71 71 |
|
||||||
72 72 | for i in list(foo_list): # OK
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
let semantic = checker.semantic();
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(id == "type" && checker.semantic().is_builtin("type")) {
|
if !semantic.match_builtin_expr(func, "type") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,11 +88,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
||||||
func, arguments, ..
|
func, arguments, ..
|
||||||
}) => {
|
}) => {
|
||||||
// Ex) `type(obj) is type(1)`
|
// Ex) `type(obj) is type(1)`
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
|
if semantic.match_builtin_expr(func, "type") {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if id == "type" && checker.semantic().is_builtin("type") {
|
|
||||||
// Allow comparison for types which are not obvious.
|
// Allow comparison for types which are not obvious.
|
||||||
if arguments
|
if arguments
|
||||||
.args
|
.args
|
||||||
|
@ -112,8 +106,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
||||||
}
|
}
|
||||||
Expr::Attribute(ast::ExprAttribute { value, .. }) => {
|
Expr::Attribute(ast::ExprAttribute { value, .. }) => {
|
||||||
// Ex) `type(obj) is types.NoneType`
|
// Ex) `type(obj) is types.NoneType`
|
||||||
if checker
|
if semantic
|
||||||
.semantic()
|
|
||||||
.resolve_qualified_name(value.as_ref())
|
.resolve_qualified_name(value.as_ref())
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(qualified_name.segments(), ["types", ..])
|
matches!(qualified_name.segments(), ["types", ..])
|
||||||
|
@ -141,7 +134,7 @@ fn deprecated_type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
|
||||||
| "dict"
|
| "dict"
|
||||||
| "set"
|
| "set"
|
||||||
| "memoryview"
|
| "memoryview"
|
||||||
) && checker.semantic().is_builtin(id)
|
) && semantic.is_builtin(id)
|
||||||
{
|
{
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
TypeComparison {
|
TypeComparison {
|
||||||
|
@ -188,20 +181,17 @@ fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
Expr::Call(ast::ExprCall {
|
Expr::Call(ast::ExprCall {
|
||||||
func, arguments, ..
|
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.
|
// Allow comparison for types which are not obvious.
|
||||||
arguments
|
if !arguments
|
||||||
.args
|
.args
|
||||||
.first()
|
.first()
|
||||||
.is_some_and(|arg| !arg.is_name_expr() && !arg.is_none_literal_expr())
|
.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, .. }) => {
|
Expr::Name(ast::ExprName { id, .. }) => {
|
||||||
// Ex) `type(obj) == int`
|
// Ex) `type(obj) == int`
|
||||||
|
|
|
@ -167,4 +167,11 @@ E721.py:117:12: E721 Do not compare types, use `isinstance()`
|
||||||
118 | ...
|
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
|
139 | #: E721
|
||||||
140 | dtype == float
|
140 | dtype == float
|
||||||
| ^^^^^^^^^^^^^^ E721
|
| ^^^^^^^^^^^^^^ 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_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::Expr;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
@ -58,16 +57,9 @@ impl Violation for InvalidPrintSyntax {
|
||||||
|
|
||||||
/// F633
|
/// F633
|
||||||
pub(crate) fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
|
pub(crate) fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = &left else {
|
if checker.semantic().match_builtin_expr(left, "print") {
|
||||||
return;
|
checker
|
||||||
};
|
.diagnostics
|
||||||
if id != "print" {
|
.push(Diagnostic::new(InvalidPrintSyntax, left.range()));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if !checker.semantic().is_builtin("print") {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
checker
|
|
||||||
.diagnostics
|
|
||||||
.push(Diagnostic::new(InvalidPrintSyntax, left.range()));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,11 +75,16 @@ pub(crate) fn raise_not_implemented(checker: &mut Checker, expr: &Expr) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range());
|
let mut diagnostic = Diagnostic::new(RaiseNotImplemented, expr.range());
|
||||||
if checker.semantic().is_builtin("NotImplementedError") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"NotImplementedError".to_string(),
|
"NotImplementedError",
|
||||||
expr.range(),
|
expr.start(),
|
||||||
)));
|
checker.semantic(),
|
||||||
}
|
)?;
|
||||||
|
Ok(Fix::safe_edits(
|
||||||
|
Edit::range_replacement(binding, expr.range()),
|
||||||
|
import_edit,
|
||||||
|
))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,5 +31,27 @@ F901.py:6:11: F901 [*] `raise NotImplemented` should be `raise NotImplementedErr
|
||||||
5 5 | def g() -> None:
|
5 5 | def g() -> None:
|
||||||
6 |- raise NotImplemented
|
6 |- raise NotImplemented
|
||||||
6 |+ raise NotImplementedError
|
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.
|
/// If a function is a call to `open`, returns the kind of `open` call.
|
||||||
fn is_open(func: &Expr, semantic: &SemanticModel) -> Option<Kind> {
|
fn is_open(func: &Expr, semantic: &SemanticModel) -> Option<Kind> {
|
||||||
match func {
|
// Ex) `open(...)`
|
||||||
// Ex) `pathlib.Path(...).open(...)`
|
if semantic.match_builtin_expr(func, "open") {
|
||||||
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
|
return Some(Kind::Builtin);
|
||||||
match value.as_ref() {
|
}
|
||||||
Expr::Call(ast::ExprCall { func, .. }) => semantic
|
|
||||||
.resolve_qualified_name(func)
|
// Ex) `pathlib.Path(...).open(...)`
|
||||||
.is_some_and(|qualified_name| {
|
let ast::ExprAttribute { attr, value, .. } = func.as_attribute_expr()?;
|
||||||
matches!(qualified_name.segments(), ["pathlib", "Path"])
|
if attr != "open" {
|
||||||
})
|
return None;
|
||||||
.then_some(Kind::Pathlib),
|
}
|
||||||
_ => None,
|
let ast::ExprCall {
|
||||||
}
|
func: value_func, ..
|
||||||
}
|
} = value.as_call_expr()?;
|
||||||
// Ex) `open(...)`
|
let qualified_name = semantic.resolve_qualified_name(value_func)?;
|
||||||
Expr::Name(ast::ExprName { id, .. }) => {
|
match qualified_name.segments() {
|
||||||
(id.as_str() == "open" && semantic.is_builtin("open")).then_some(Kind::Builtin)
|
["pathlib", "Path"] => Some(Kind::Pathlib),
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,27 +96,20 @@ impl std::fmt::Display for Nan {
|
||||||
|
|
||||||
/// Returns `true` if the expression is a call to `float("NaN")`.
|
/// Returns `true` if the expression is a call to `float("NaN")`.
|
||||||
fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool {
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() else {
|
if !keywords.is_empty() {
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if id.as_str() != "float" {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !call.arguments.keywords.is_empty() {
|
let [Expr::StringLiteral(ast::ExprStringLiteral { value, .. })] = &**args else {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [arg] = call.arguments.args.as_ref() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -127,9 +120,5 @@ fn is_nan_float(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !semantic.is_builtin("float") {
|
semantic.match_builtin_expr(func, "float")
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,15 +68,10 @@ impl MinMax {
|
||||||
if !keywords.is_empty() {
|
if !keywords.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = func else {
|
match semantic.resolve_builtin_symbol(func)? {
|
||||||
return None;
|
"min" => Some(Self::Min),
|
||||||
};
|
"max" => Some(Self::Max),
|
||||||
if id.as_str() == "min" && semantic.is_builtin("min") {
|
_ => None,
|
||||||
Some(MinMax::Min)
|
|
||||||
} else if id.as_str() == "max" && semantic.is_builtin("max") {
|
|
||||||
Some(MinMax::Max)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,14 +61,15 @@ impl Violation for NonSlotAssignment {
|
||||||
|
|
||||||
/// E0237
|
/// E0237
|
||||||
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
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
|
// If the class inherits from another class (aside from `object`), then it's possible that
|
||||||
// the parent class defines the relevant `__slots__`.
|
// the parent class defines the relevant `__slots__`.
|
||||||
if !class_def.bases().iter().all(|base| {
|
if !class_def
|
||||||
checker
|
.bases()
|
||||||
.semantic()
|
.iter()
|
||||||
.resolve_qualified_name(base)
|
.all(|base| semantic.match_builtin_expr(base, "object"))
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "object"]))
|
{
|
||||||
}) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use ruff_python_ast::{self as ast, Decorator, Expr, Parameters, Stmt};
|
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, 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;
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
@ -53,9 +51,10 @@ pub(crate) fn property_with_parameters(
|
||||||
decorator_list: &[Decorator],
|
decorator_list: &[Decorator],
|
||||||
parameters: &Parameters,
|
parameters: &Parameters,
|
||||||
) {
|
) {
|
||||||
|
let semantic = checker.semantic();
|
||||||
if !decorator_list
|
if !decorator_list
|
||||||
.iter()
|
.iter()
|
||||||
.any(|decorator| matches!(&decorator.expression, Expr::Name(ast::ExprName { id, .. }) if id == "property"))
|
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "property"))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,6 @@ pub(crate) fn property_with_parameters(
|
||||||
.chain(¶meters.kwonlyargs)
|
.chain(¶meters.kwonlyargs)
|
||||||
.count()
|
.count()
|
||||||
> 1
|
> 1
|
||||||
&& checker.semantic().is_builtin("property")
|
|
||||||
{
|
{
|
||||||
checker
|
checker
|
||||||
.diagnostics
|
.diagnostics
|
||||||
|
|
|
@ -92,14 +92,11 @@ pub(crate) fn repeated_isinstance_calls(
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !matches!(func.as_ref(), Expr::Name(ast::ExprName { id, .. }) if id == "isinstance") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let [obj, types] = &args[..] else {
|
let [obj, types] = &args[..] else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !checker.semantic().is_builtin("isinstance") {
|
if !checker.semantic().match_builtin_expr(func, "isinstance") {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
let (num_calls, matches) = obj_to_types
|
let (num_calls, matches) = obj_to_types
|
||||||
.entry(obj.into())
|
.entry(obj.into())
|
||||||
|
|
|
@ -124,16 +124,6 @@ fn enumerate_items<'a>(
|
||||||
func, arguments, ..
|
func, arguments, ..
|
||||||
} = call_expr.as_call_expr()?;
|
} = 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 {
|
let Expr::Tuple(ast::ExprTuple { elts, .. }) = tuple_expr else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
@ -161,6 +151,11 @@ fn enumerate_items<'a>(
|
||||||
return None;
|
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))
|
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
|
| ^^^^^^^ PLW0177
|
||||||
48 | pass
|
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]
|
33 | pathlib.Path(NAME).open(mode="rwx") # [bad-open-mode]
|
||||||
34 | pathlib.Path(NAME).open("rwx", encoding="utf-8") # [bad-open-mode]
|
34 | pathlib.Path(NAME).open("rwx", encoding="utf-8") # [bad-open-mode]
|
||||||
| ^^^^^ PLW1501
|
| ^^^^^ 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.
|
40 | # Starred argument should be copied as it is.
|
||||||
41 | max(1, max(*a))
|
41 | max(1, max(*a))
|
||||||
| ^^^^^^^^^^^^^^^ PLW3301
|
| ^^^^^^^^^^^^^^^ PLW3301
|
||||||
|
42 |
|
||||||
|
43 | import builtins
|
||||||
|
|
|
|
||||||
= help: Flatten nested `max` calls
|
= 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.
|
40 40 | # Starred argument should be copied as it is.
|
||||||
41 |-max(1, max(*a))
|
41 |-max(1, max(*a))
|
||||||
41 |+max(1, *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"]))
|
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["io", "open"]))
|
||||||
{
|
{
|
||||||
let mut diagnostic = Diagnostic::new(OpenAlias, expr.range());
|
let mut diagnostic = Diagnostic::new(OpenAlias, expr.range());
|
||||||
if checker.semantic().is_builtin("open") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"open".to_string(),
|
"open",
|
||||||
func.range(),
|
expr.start(),
|
||||||
)));
|
checker.semantic(),
|
||||||
}
|
)?;
|
||||||
|
Ok(Fix::safe_edits(
|
||||||
|
Edit::range_replacement(binding, func.range()),
|
||||||
|
import_edit,
|
||||||
|
))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,19 +61,14 @@ fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
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`].
|
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
|
||||||
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
@ -82,19 +77,25 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||||
},
|
},
|
||||||
target.range(),
|
target.range(),
|
||||||
);
|
);
|
||||||
if checker.semantic().is_builtin("OSError") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"OSError".to_string(),
|
"OSError",
|
||||||
target.range(),
|
target.start(),
|
||||||
)));
|
checker.semantic(),
|
||||||
}
|
)?;
|
||||||
|
Ok(Fix::safe_edits(
|
||||||
|
Edit::range_replacement(binding, target.range()),
|
||||||
|
import_edit,
|
||||||
|
))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||||
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
||||||
let mut diagnostic = Diagnostic::new(OSErrorAlias { name: None }, tuple.range());
|
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.
|
// Filter out any `OSErrors` aliases.
|
||||||
let mut remaining: Vec<Expr> = tuple
|
let mut remaining: Vec<Expr> = tuple
|
||||||
.elts
|
.elts
|
||||||
|
@ -112,7 +113,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
|
||||||
if tuple
|
if tuple
|
||||||
.elts
|
.elts
|
||||||
.iter()
|
.iter()
|
||||||
.all(|elt| !is_os_error(elt, checker.semantic()))
|
.all(|elt| !semantic.match_builtin_expr(elt, "OSError"))
|
||||||
{
|
{
|
||||||
let node = ast::ExprName {
|
let node = ast::ExprName {
|
||||||
id: "OSError".into(),
|
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() {
|
for base in arguments.args.iter() {
|
||||||
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) {
|
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) {
|
||||||
match qualified_name.segments() {
|
match qualified_name.segments() {
|
||||||
["", "str"] => inherits_str = true,
|
["" | "builtins", "str"] => inherits_str = true,
|
||||||
["enum", "Enum"] => inherits_enum = 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`].
|
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
|
||||||
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
@ -96,19 +89,25 @@ fn atom_diagnostic(checker: &mut Checker, target: &Expr) {
|
||||||
},
|
},
|
||||||
target.range(),
|
target.range(),
|
||||||
);
|
);
|
||||||
if checker.semantic().is_builtin("TimeoutError") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"TimeoutError".to_string(),
|
"TimeoutError",
|
||||||
target.range(),
|
target.start(),
|
||||||
)));
|
checker.semantic(),
|
||||||
}
|
)?;
|
||||||
|
Ok(Fix::safe_edits(
|
||||||
|
Edit::range_replacement(binding, target.range()),
|
||||||
|
import_edit,
|
||||||
|
))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [`Diagnostic`] for a tuple of expressions.
|
/// Create a [`Diagnostic`] for a tuple of expressions.
|
||||||
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
|
||||||
let mut diagnostic = Diagnostic::new(TimeoutErrorAlias { name: None }, tuple.range());
|
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.
|
// Filter out any `TimeoutErrors` aliases.
|
||||||
let mut remaining: Vec<Expr> = tuple
|
let mut remaining: Vec<Expr> = tuple
|
||||||
.elts
|
.elts
|
||||||
|
@ -126,7 +125,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
|
||||||
if tuple
|
if tuple
|
||||||
.elts
|
.elts
|
||||||
.iter()
|
.iter()
|
||||||
.all(|elt| !is_timeout_error(elt, checker.semantic()))
|
.all(|elt| !semantic.match_builtin_expr(elt, "TimeoutError"))
|
||||||
{
|
{
|
||||||
let node = ast::ExprName {
|
let node = ast::ExprName {
|
||||||
id: "TimeoutError".into(),
|
id: "TimeoutError".into(),
|
||||||
|
|
|
@ -58,19 +58,16 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr,
|
||||||
let [arg] = args else {
|
let [arg] = args else {
|
||||||
return;
|
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 {
|
let Some(primitive) = Primitive::from_expr(arg) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
if !semantic.match_builtin_expr(func, "type") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range());
|
let mut diagnostic = Diagnostic::new(TypeOfPrimitive { primitive }, expr.range());
|
||||||
let builtin = primitive.builtin();
|
let builtin = primitive.builtin();
|
||||||
if checker.semantic().is_builtin(&builtin) {
|
if semantic.is_builtin(&builtin) {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||||
pad(primitive.builtin(), expr.range(), checker.locator()),
|
pad(primitive.builtin(), expr.range(), checker.locator()),
|
||||||
expr.range(),
|
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"]))
|
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
|
||||||
{
|
{
|
||||||
let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range());
|
let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range());
|
||||||
if checker.semantic().is_builtin("str") {
|
diagnostic.try_set_fix(|| {
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
|
||||||
"str".to_string(),
|
"str",
|
||||||
expr.range(),
|
expr.start(),
|
||||||
)));
|
checker.semantic(),
|
||||||
}
|
)?;
|
||||||
|
Ok(Fix::safe_edits(
|
||||||
|
Edit::range_replacement(binding, expr.range()),
|
||||||
|
import_edit,
|
||||||
|
))
|
||||||
|
});
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
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 ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
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() {
|
for base in arguments.args.iter() {
|
||||||
let Expr::Name(ast::ExprName { id, .. }) = base else {
|
if !checker.semantic().match_builtin_expr(base, "object") {
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if id != "object" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !checker.semantic().is_builtin("object") {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -490,4 +490,20 @@ UP004.py:146:9: UP004 [*] Class `A` inherits from `object`
|
||||||
148 148 |
|
148 148 |
|
||||||
149 149 |
|
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 |
|
5 5 |
|
||||||
6 6 | from io import open
|
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
|
6 | from io import open
|
||||||
7 |
|
7 |
|
||||||
|
@ -30,4 +30,12 @@ UP020.py:8:6: UP020 Use builtin `open`
|
||||||
|
|
|
|
||||||
= help: Replace with 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()?;
|
} = 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()?;
|
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")`,
|
// 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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !semantic.match_builtin_expr(func, "open") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// Match positional arguments, get filename and mode.
|
// Match positional arguments, get filename and mode.
|
||||||
let (filename, pos_mode) = match_open_args(args)?;
|
let (filename, pos_mode) = match_open_args(args)?;
|
||||||
|
|
||||||
|
|
|
@ -97,15 +97,6 @@ pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
|
||||||
return;
|
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() {
|
if !arguments.keywords.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -113,6 +104,11 @@ pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ensure that we're performing a `bin(...)`.
|
||||||
|
if !checker.semantic().match_builtin_expr(func, "bin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract, e.g., `x` in `bin(x)`.
|
// Extract, e.g., `x` in `bin(x)`.
|
||||||
let literal_text = checker.locator().slice(arg);
|
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) {
|
pub(crate) fn int_on_sliced_str(checker: &mut Checker, call: &ExprCall) {
|
||||||
// Verify that the function is `int`.
|
// Verify that the function is `int`.
|
||||||
let Expr::Name(name) = call.func.as_ref() else {
|
if !checker.semantic().match_builtin_expr(&call.func, "int") {
|
||||||
return;
|
|
||||||
};
|
|
||||||
if name.id.as_str() != "int" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !checker.semantic().is_builtin("int") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,17 +141,11 @@ fn extract_name_from_reversed<'a>(
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg = func
|
if !semantic.match_builtin_expr(func, "reversed") {
|
||||||
.as_name_expr()
|
|
||||||
.is_some_and(|name| name.id == "reversed")
|
|
||||||
.then(|| arg.as_name_expr())
|
|
||||||
.flatten()?;
|
|
||||||
|
|
||||||
if !semantic.is_builtin("reversed") {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(arg)
|
arg.as_name_expr()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a slice expression, returns the inner argument if it's a reversed slice.
|
/// Given a slice expression, returns the inner argument if it's a reversed slice.
|
||||||
|
|
|
@ -70,12 +70,7 @@ impl Violation for PrintEmptyString {
|
||||||
|
|
||||||
/// FURB105
|
/// FURB105
|
||||||
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
|
pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) {
|
||||||
if !checker
|
if !checker.semantic().match_builtin_expr(&call.func, "print") {
|
||||||
.semantic()
|
|
||||||
.resolve_qualified_name(&call.func)
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"]))
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl Violation for ReadWholeFile {
|
||||||
/// FURB101
|
/// FURB101
|
||||||
pub(crate) fn read_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
pub(crate) fn read_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
||||||
// `async` check here is more of a precaution.
|
// `async` check here is more of a precaution.
|
||||||
if with.is_async || !checker.semantic().is_builtin("open") {
|
if with.is_async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,22 +102,16 @@ pub(crate) fn unnecessary_enumerate(checker: &mut Checker, stmt_for: &ast::StmtF
|
||||||
return;
|
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.
|
// Get the first argument, which is the sequence to iterate over.
|
||||||
let Some(Expr::Name(sequence)) = arguments.args.first() else {
|
let Some(Expr::Name(sequence)) = arguments.args.first() else {
|
||||||
return;
|
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.
|
// Check if the index and value are used.
|
||||||
match (
|
match (
|
||||||
checker.semantic().is_unused(index),
|
checker.semantic().is_unused(index),
|
||||||
|
|
|
@ -71,20 +71,19 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
|
||||||
// The value must be either `decimal.Decimal` or `fractions.Fraction`.
|
// The value must be either `decimal.Decimal` or `fractions.Fraction`.
|
||||||
let Some(constructor) =
|
let Some(qualified_name) = semantic.resolve_qualified_name(value) else {
|
||||||
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 {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let constructor = match qualified_name.segments() {
|
||||||
|
["decimal", "Decimal"] => Constructor::Decimal,
|
||||||
|
["fractions", "Fraction"] => Constructor::Fraction,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
// `Decimal.from_decimal` doesn't exist.
|
// `Decimal.from_decimal` doesn't exist.
|
||||||
if matches!(
|
if matches!(
|
||||||
(method_name, constructor),
|
(method_name, constructor),
|
||||||
|
@ -131,14 +130,6 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
||||||
break 'short_circuit;
|
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.
|
// Must have exactly one argument, which is a string literal.
|
||||||
if arguments.keywords.len() != 0 {
|
if arguments.keywords.len() != 0 {
|
||||||
break 'short_circuit;
|
break 'short_circuit;
|
||||||
|
@ -156,7 +147,8 @@ pub(crate) fn unnecessary_from_float(checker: &mut Checker, call: &ExprCall) {
|
||||||
break 'short_circuit;
|
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;
|
break 'short_circuit;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -116,10 +116,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp
|
||||||
func, arguments, ..
|
func, arguments, ..
|
||||||
}) => {
|
}) => {
|
||||||
// Must be a call to the `float` builtin.
|
// Must be a call to the `float` builtin.
|
||||||
let Some(func_name) = func.as_name_expr() else {
|
if !checker.semantic().match_builtin_expr(func, "float") {
|
||||||
return;
|
|
||||||
};
|
|
||||||
if func_name.id != "float" {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,10 +137,6 @@ pub(crate) fn verbose_decimal_constructor(checker: &mut Checker, call: &ast::Exp
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checker.semantic().is_builtin("float") {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let replacement = checker.locator().slice(float).to_string();
|
let replacement = checker.locator().slice(float).to_string();
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
VerboseDecimalConstructor {
|
VerboseDecimalConstructor {
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl Violation for WriteWholeFile {
|
||||||
/// FURB103
|
/// FURB103
|
||||||
pub(crate) fn write_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
pub(crate) fn write_whole_file(checker: &mut Checker, with: &ast::StmtWith) {
|
||||||
// `async` check here is more of a precaution.
|
// `async` check here is more of a precaution.
|
||||||
if with.is_async || !checker.semantic().is_builtin("open") {
|
if with.is_async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,3 +92,19 @@ FURB103.py:58:6: FURB103 `open` and `write` should be replaced by `Path("file.tx
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB103
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB103
|
||||||
59 | f.write(foobar)
|
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():
|
40 40 | for _line in bar.readlines():
|
||||||
41 41 | pass
|
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" {
|
if attr != "fromkeys" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(name_expr) = value.as_name_expr() else {
|
let semantic = checker.semantic();
|
||||||
return;
|
if !semantic.match_builtin_expr(value, "dict") {
|
||||||
};
|
|
||||||
if name_expr.id != "dict" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !checker.semantic().is_builtin("dict") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +82,7 @@ pub(crate) fn mutable_fromkeys_value(checker: &mut Checker, call: &ast::ExprCall
|
||||||
let [keys, value] = &*call.arguments.args else {
|
let [keys, value] = &*call.arguments.args else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !is_mutable_expr(value, checker.semantic()) {
|
if !is_mutable_expr(value, semantic) {
|
||||||
return;
|
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}
|
12 |+{key: set() for key in pierogi_fillings}
|
||||||
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
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`
|
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!"})
|
13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
||||||
14 | dict.fromkeys(pierogi_fillings, dict())
|
14 | dict.fromkeys(pierogi_fillings, dict())
|
||||||
|
15 | import builtins
|
||||||
|
|
|
|
||||||
= help: Replace with comprehension
|
= 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 |-dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
13 |+{key: {"pre": "populated!"} for key in pierogi_fillings}
|
13 |+{key: {"pre": "populated!"} for key in pierogi_fillings}
|
||||||
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
14 14 | dict.fromkeys(pierogi_fillings, dict())
|
||||||
15 15 |
|
15 15 | import builtins
|
||||||
16 16 | # Okay.
|
16 16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||||
|
|
||||||
RUF024.py:14:1: RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys`
|
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!"})
|
13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
14 | dict.fromkeys(pierogi_fillings, dict())
|
14 | dict.fromkeys(pierogi_fillings, dict())
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF024
|
||||||
15 |
|
15 | import builtins
|
||||||
16 | # Okay.
|
16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||||
|
|
|
|
||||||
= help: Replace with comprehension
|
= 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!"})
|
13 13 | dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
|
||||||
14 |-dict.fromkeys(pierogi_fillings, dict())
|
14 |-dict.fromkeys(pierogi_fillings, dict())
|
||||||
14 |+{key: dict() for key in pierogi_fillings}
|
14 |+{key: dict() for key in pierogi_fillings}
|
||||||
15 15 |
|
15 15 | import builtins
|
||||||
16 16 | # Okay.
|
16 16 | builtins.dict.fromkeys(pierogi_fillings, dict())
|
||||||
17 17 | dict.fromkeys(pierogi_fillings)
|
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`.
|
// `NotImplementedError`.
|
||||||
if checker
|
if checker
|
||||||
.semantic()
|
.semantic()
|
||||||
.resolve_qualified_name(func)
|
.match_builtin_expr(func, "NotImplementedError")
|
||||||
.is_some_and(|qualified_name| {
|
|
||||||
matches!(qualified_name.segments(), ["", "NotImplementedError"])
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,15 +63,12 @@ impl Violation for RaiseVanillaClass {
|
||||||
|
|
||||||
/// TRY002
|
/// TRY002
|
||||||
pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) {
|
pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) {
|
||||||
if checker
|
let node = if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
||||||
.semantic()
|
func
|
||||||
.resolve_qualified_name(if let Expr::Call(ast::ExprCall { func, .. }) = expr {
|
} else {
|
||||||
func
|
expr
|
||||||
} else {
|
};
|
||||||
expr
|
if checker.semantic().match_builtin_expr(node, "Exception") {
|
||||||
})
|
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "Exception"]))
|
|
||||||
{
|
|
||||||
checker
|
checker
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.push(Diagnostic::new(RaiseVanillaClass, expr.range()));
|
.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| {
|
|| handled_exceptions.iter().any(|expr| {
|
||||||
checker
|
checker
|
||||||
.semantic()
|
.semantic()
|
||||||
.resolve_qualified_name(expr)
|
.resolve_builtin_symbol(expr)
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|builtin| matches!(builtin, "Exception" | "BaseException"))
|
||||||
matches!(
|
|
||||||
qualified_name.segments(),
|
|
||||||
["", "Exception" | "BaseException"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
checker
|
checker
|
||||||
|
|
|
@ -74,26 +74,20 @@ fn has_control_flow(stmt: &Stmt) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if an [`Expr`] is a call to check types.
|
/// Returns `true` if an [`Expr`] is a call to check types.
|
||||||
fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool {
|
fn check_type_check_call(semantic: &SemanticModel, call: &Expr) -> bool {
|
||||||
checker
|
semantic
|
||||||
.semantic()
|
.resolve_builtin_symbol(call)
|
||||||
.resolve_qualified_name(call)
|
.is_some_and(|builtin| matches!(builtin, "isinstance" | "issubclass" | "callable"))
|
||||||
.is_some_and(|qualified_name| {
|
|
||||||
matches!(
|
|
||||||
qualified_name.segments(),
|
|
||||||
["", "isinstance" | "issubclass" | "callable"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if an [`Expr`] is a test to check types (e.g. via isinstance)
|
/// 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 {
|
match test {
|
||||||
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values
|
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => values
|
||||||
.iter()
|
.iter()
|
||||||
.all(|expr| check_type_check_test(checker, expr)),
|
.all(|expr| check_type_check_test(semantic, expr)),
|
||||||
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => check_type_check_test(checker, operand),
|
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => check_type_check_test(semantic, operand),
|
||||||
Expr::Call(ast::ExprCall { func, .. }) => check_type_check_call(checker, func),
|
Expr::Call(ast::ExprCall { func, .. }) => check_type_check_call(semantic, func),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,14 +155,15 @@ pub(crate) fn type_check_without_type_error(
|
||||||
elif_else_clauses,
|
elif_else_clauses,
|
||||||
..
|
..
|
||||||
} = stmt_if;
|
} = stmt_if;
|
||||||
|
|
||||||
if let Some(Stmt::If(ast::StmtIf { test, .. })) = parent {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only consider the body when the `if` condition is all type-related
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
check_body(checker, body);
|
check_body(checker, body);
|
||||||
|
@ -176,7 +171,7 @@ pub(crate) fn type_check_without_type_error(
|
||||||
for clause in elif_else_clauses {
|
for clause in elif_else_clauses {
|
||||||
if let Some(test) = &clause.test {
|
if let Some(test) = &clause.test {
|
||||||
// If there are any `elif`, they must all also be type-related
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,9 +47,15 @@ impl<'a> QualifiedName<'a> {
|
||||||
self.0.as_slice()
|
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"`
|
/// Ex) `["", "bool"]` -> `"bool"`
|
||||||
pub fn is_builtin(&self) -> bool {
|
fn is_builtin(&self) -> bool {
|
||||||
matches!(self.segments(), ["", ..])
|
matches!(self.segments(), ["", ..])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub fn classify(
|
||||||
semantic
|
semantic
|
||||||
.resolve_qualified_name(map_callable(expr))
|
.resolve_qualified_name(map_callable(expr))
|
||||||
.is_some_and( |qualified_name| {
|
.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))
|
|| 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| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
["" | "builtins", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
||||||
) || staticmethod_decorators
|
) || staticmethod_decorators
|
||||||
.iter()
|
.iter()
|
||||||
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
||||||
|
@ -103,7 +103,7 @@ fn is_class_method(
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["", "classmethod"] | ["abc", "abstractclassmethod"]
|
["" | "builtins", "classmethod"] | ["abc", "abstractclassmethod"]
|
||||||
) || classmethod_decorators
|
) || classmethod_decorators
|
||||||
.iter()
|
.iter()
|
||||||
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
|
||||||
|
|
|
@ -686,7 +686,7 @@ impl TypeChecker for IoBaseChecker {
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
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".
|
/// Returns `true` if a function is a "static method".
|
||||||
pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
||||||
decorator_list.iter().any(|decorator| {
|
decorator_list
|
||||||
semantic
|
.iter()
|
||||||
.resolve_qualified_name(map_callable(&decorator.expression))
|
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "staticmethod"))
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "staticmethod"]))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if a function is a "class method".
|
/// Returns `true` if a function is a "class method".
|
||||||
pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
|
||||||
decorator_list.iter().any(|decorator| {
|
decorator_list
|
||||||
semantic
|
.iter()
|
||||||
.resolve_qualified_name(map_callable(&decorator.expression))
|
.any(|decorator| semantic.match_builtin_expr(&decorator.expression, "classmethod"))
|
||||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "classmethod"]))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if a function definition is an `@overload`.
|
/// Returns `true` if a function definition is an `@overload`.
|
||||||
|
@ -79,7 +75,7 @@ pub fn is_property(
|
||||||
.is_some_and(|qualified_name| {
|
.is_some_and(|qualified_name| {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name.segments(),
|
qualified_name.segments(),
|
||||||
["", "property"] | ["functools", "cached_property"]
|
["" | "builtins", "property"] | ["functools", "cached_property"]
|
||||||
) || extra_properties
|
) || extra_properties
|
||||||
.iter()
|
.iter()
|
||||||
.any(|extra_property| extra_property.segments() == qualified_name.segments())
|
.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.
|
/// 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 {
|
pub fn is_builtin(&self, member: &str) -> bool {
|
||||||
self.lookup_symbol(member)
|
self.lookup_symbol(member)
|
||||||
.map(|binding_id| &self.bindings[binding_id])
|
.map(|binding_id| &self.bindings[binding_id])
|
||||||
.is_some_and(|binding| binding.kind.is_builtin())
|
.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
|
/// 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.
|
/// in the current scope, or in any containing scope.
|
||||||
pub fn is_available(&self, member: &str) -> bool {
|
pub fn is_available(&self, member: &str) -> bool {
|
||||||
|
@ -1138,6 +1179,7 @@ impl<'a> SemanticModel<'a> {
|
||||||
pub fn add_module(&mut self, module: &str) {
|
pub fn add_module(&mut self, module: &str) {
|
||||||
match module {
|
match module {
|
||||||
"_typeshed" => self.seen.insert(Modules::TYPESHED),
|
"_typeshed" => self.seen.insert(Modules::TYPESHED),
|
||||||
|
"builtins" => self.seen.insert(Modules::BUILTINS),
|
||||||
"collections" => self.seen.insert(Modules::COLLECTIONS),
|
"collections" => self.seen.insert(Modules::COLLECTIONS),
|
||||||
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
|
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
|
||||||
"datetime" => self.seen.insert(Modules::DATETIME),
|
"datetime" => self.seen.insert(Modules::DATETIME),
|
||||||
|
@ -1708,6 +1750,7 @@ bitflags! {
|
||||||
const TYPING_EXTENSIONS = 1 << 15;
|
const TYPING_EXTENSIONS = 1 << 15;
|
||||||
const TYPESHED = 1 << 16;
|
const TYPESHED = 1 << 16;
|
||||||
const DATACLASSES = 1 << 17;
|
const DATACLASSES = 1 << 17;
|
||||||
|
const BUILTINS = 1 << 18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
pub fn is_standard_library_generic(qualified_name: &[&str]) -> bool {
|
pub fn is_standard_library_generic(qualified_name: &[&str]) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name,
|
qualified_name,
|
||||||
["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"]
|
[
|
||||||
| [
|
"" | "builtins",
|
||||||
"collections" | "typing" | "typing_extensions",
|
"dict" | "frozenset" | "list" | "set" | "tuple" | "type"
|
||||||
"ChainMap" | "Counter"
|
] | [
|
||||||
]
|
"collections" | "typing" | "typing_extensions",
|
||||||
| ["collections" | "typing", "OrderedDict"]
|
"ChainMap" | "Counter"
|
||||||
|
] | ["collections" | "typing", "OrderedDict"]
|
||||||
| ["collections", "defaultdict" | "deque"]
|
| ["collections", "defaultdict" | "deque"]
|
||||||
| [
|
| [
|
||||||
"collections",
|
"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 {
|
pub fn is_immutable_generic_type(qualified_name: &[&str]) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name,
|
qualified_name,
|
||||||
["", "tuple"]
|
["" | "builtins", "tuple"]
|
||||||
| [
|
| [
|
||||||
"collections",
|
"collections",
|
||||||
"abc",
|
"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 {
|
pub fn is_mutable_return_type(qualified_name: &[&str]) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
qualified_name,
|
qualified_name,
|
||||||
["", "dict" | "list" | "set"]
|
["" | "builtins", "dict" | "list" | "set"]
|
||||||
| [
|
| [
|
||||||
"collections",
|
"collections",
|
||||||
"Counter" | "OrderedDict" | "defaultdict" | "deque"
|
"Counter" | "OrderedDict" | "defaultdict" | "deque"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue