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:
Alex Waygood 2024-04-16 11:37:31 +01:00 committed by GitHub
parent effd5188c9
commit f779babc5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 886 additions and 588 deletions

View file

@ -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):

View file

@ -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")

View file

@ -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])

View file

@ -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)

View file

@ -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: ...

View file

@ -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

View file

@ -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

View file

@ -138,3 +138,8 @@ np.dtype(int) == float
#: E721 #: E721
dtype == float dtype == float
import builtins
if builtins.type(res) == memoryview: # E721
pass

View file

@ -4,3 +4,8 @@ def f() -> None:
def g() -> None: def g() -> None:
raise NotImplemented raise NotImplemented
def h() -> None:
NotImplementedError = "foo"
raise NotImplemented

View file

@ -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")

View file

@ -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

View file

@ -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))

View file

@ -152,3 +152,9 @@ object = A
class B(object): class B(object):
... ...
import builtins
class Unusual(builtins.object):
...

View file

@ -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:

View file

@ -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():

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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",

View file

@ -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"]
)
})
} }
}) })
} }

View file

@ -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()

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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);
} }

View file

@ -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); );
}
} }
} }

View file

@ -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 |

View file

@ -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

View file

@ -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)

View file

@ -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"]))
} }

View file

@ -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) {

View file

@ -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(

View file

@ -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)

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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
} }

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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 {
// pathlib.Path(...).open()
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
match value.as_ref() {
Expr::Call(ast::ExprCall { func, .. }) => checker
.semantic()
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path"])
}),
_ => false,
}
}
// open(...) // open(...)
Expr::Name(ast::ExprName { id, .. }) => { if semantic.match_builtin_expr(func, "open") {
id.as_str() == "open" && checker.semantic().is_builtin("open") return true;
} }
_ => false,
// pathlib.Path(...).open()
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
return false;
};
if attr != "open" {
return false;
} }
let Expr::Call(ast::ExprCall {
func: value_func, ..
}) = &**value
else {
return false;
};
semantic
.resolve_qualified_name(value_func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pathlib", "Path"]))
} }
/// Return `true` if the current expression is followed by a `close` call. /// 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;
} }

View file

@ -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

View file

@ -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
|

View file

@ -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;
} }

View file

@ -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

View file

@ -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`

View file

@ -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
|

View file

@ -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
|

View file

@ -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;
};
if id != "print" {
return;
}
if !checker.semantic().is_builtin("print") {
return;
};
checker checker
.diagnostics .diagnostics
.push(Diagnostic::new(InvalidPrintSyntax, left.range())); .push(Diagnostic::new(InvalidPrintSyntax, left.range()));
}
} }

View file

@ -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);
} }

View file

@ -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

View file

@ -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) `pathlib.Path(...).open(...)`
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => {
match value.as_ref() {
Expr::Call(ast::ExprCall { func, .. }) => semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path"])
})
.then_some(Kind::Pathlib),
_ => None,
}
}
// Ex) `open(...)` // Ex) `open(...)`
Expr::Name(ast::ExprName { id, .. }) => { if semantic.match_builtin_expr(func, "open") {
(id.as_str() == "open" && semantic.is_builtin("open")).then_some(Kind::Builtin) return Some(Kind::Builtin);
} }
// Ex) `pathlib.Path(...).open(...)`
let ast::ExprAttribute { attr, value, .. } = func.as_attribute_expr()?;
if attr != "open" {
return None;
}
let ast::ExprCall {
func: value_func, ..
} = value.as_call_expr()?;
let qualified_name = semantic.resolve_qualified_name(value_func)?;
match qualified_name.segments() {
["pathlib", "Path"] => Some(Kind::Pathlib),
_ => None, _ => None,
} }
} }

View file

@ -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
} }

View file

@ -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
} }
} }
} }

View file

@ -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;
} }

View file

@ -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(&parameters.kwonlyargs) .chain(&parameters.kwonlyargs)
.count() .count()
> 1 > 1
&& checker.semantic().is_builtin("property")
{ {
checker checker
.diagnostics .diagnostics

View file

@ -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())

View file

@ -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))
} }

View file

@ -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
|

View file

@ -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
|

View file

@ -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)

View file

@ -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);
} }
} }

View file

@ -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(),

View file

@ -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,
_ => {} _ => {}
} }

View file

@ -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(),

View file

@ -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(),

View file

@ -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);
} }
} }

View file

@ -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;
} }

View file

@ -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 | ...

View file

@ -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())

View file

@ -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)?;

View file

@ -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);

View file

@ -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;
} }

View file

@ -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.

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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),

View file

@ -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;
}; };

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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)
|

View file

@ -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 |

View file

@ -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;
} }

View file

@ -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)

View file

@ -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;
} }

View file

@ -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()
.resolve_qualified_name(if let Expr::Call(ast::ExprCall { func, .. }) = expr {
func func
} else { } else {
expr expr
}) };
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "Exception"])) if checker.semantic().match_builtin_expr(node, "Exception") {
{
checker checker
.diagnostics .diagnostics
.push(Diagnostic::new(RaiseVanillaClass, expr.range())); .push(Diagnostic::new(RaiseVanillaClass, expr.range()));

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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(), ["", ..])
} }

View file

@ -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))

View file

@ -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"]
) )
}) })
} }

View file

@ -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())

View file

@ -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;
} }
} }

View file

@ -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",
"dict" | "frozenset" | "list" | "set" | "tuple" | "type"
] | [
"collections" | "typing" | "typing_extensions", "collections" | "typing" | "typing_extensions",
"ChainMap" | "Counter" "ChainMap" | "Counter"
] ] | ["collections" | "typing", "OrderedDict"]
| ["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"