Split CallPath into QualifiedName and UnqualifiedName (#10210)

## Summary

Charlie can probably explain this better than I but it turns out,
`CallPath` is used for two different things:

* To represent unqualified names like `version` where `version` can be a
local variable or imported (e.g. `from sys import version` where the
full qualified name is `sys.version`)
* To represent resolved, full qualified names

This PR splits `CallPath` into two types to make this destinction clear.

> Note: I haven't renamed all `call_path` variables to `qualified_name`
or `unqualified_name`. I can do that if that's welcomed but I first want
to get feedback on the approach and naming overall.

## Test Plan

`cargo test`
This commit is contained in:
Micha Reiser 2024-03-04 10:06:51 +01:00 committed by GitHub
parent ba4328226d
commit a6d892b1f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
181 changed files with 1692 additions and 1412 deletions

View file

@ -1,28 +1,28 @@
use rustc_hash::FxHashSet;
use ruff_python_ast as ast;
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;
use crate::{BindingId, SemanticModel};
/// Return `true` if any base class matches a [`CallPath`] predicate.
pub fn any_call_path(
/// Return `true` if any base class matches a [`QualifiedName`] predicate.
pub fn any_qualified_name(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(CallPath) -> bool,
func: &dyn Fn(QualifiedName) -> bool,
) -> bool {
fn inner(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(CallPath) -> bool,
func: &dyn Fn(QualifiedName) -> bool,
seen: &mut FxHashSet<BindingId>,
) -> bool {
class_def.bases().iter().any(|expr| {
// If the base class itself matches the pattern, then this does too.
// Ex) `class Foo(BaseModel): ...`
if semantic
.resolve_call_path(map_subscript(expr))
.resolve_qualified_name(map_subscript(expr))
.is_some_and(func)
{
return true;

View file

@ -1,5 +1,5 @@
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::Decorator;
use crate::model::SemanticModel;
@ -35,9 +35,9 @@ pub fn classify(
|| class_def.bases().iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
semantic
.resolve_call_path(map_callable(expr))
.is_some_and( |call_path| {
matches!(call_path.segments(), ["", "type"] | ["abc", "ABCMeta"])
.resolve_qualified_name(map_callable(expr))
.is_some_and( |qualified_name| {
matches!(qualified_name.segments(), ["", "type"] | ["abc", "ABCMeta"])
})
})
|| decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators))
@ -59,14 +59,14 @@ fn is_static_method(
// The decorator is an import, so should match against a qualified path.
if semantic
.resolve_call_path(decorator)
.is_some_and(|call_path| {
.resolve_qualified_name(decorator)
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
) || staticmethod_decorators
.iter()
.any(|decorator| call_path == CallPath::from_qualified_name(decorator))
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
})
{
return true;
@ -75,8 +75,8 @@ fn is_static_method(
// We do not have a resolvable call path, most likely from a decorator like
// `@someproperty.setter`. Instead, match on the last element.
if !staticmethod_decorators.is_empty() {
if CallPath::from_expr(decorator).is_some_and(|call_path| {
call_path.segments().last().is_some_and(|tail| {
if UnqualifiedName::from_expr(decorator).is_some_and(|name| {
name.segments().last().is_some_and(|tail| {
staticmethod_decorators
.iter()
.any(|decorator| tail == decorator)
@ -99,14 +99,14 @@ fn is_class_method(
// The decorator is an import, so should match against a qualified path.
if semantic
.resolve_call_path(decorator)
.is_some_and(|call_path| {
.resolve_qualified_name(decorator)
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
["", "classmethod"] | ["abc", "abstractclassmethod"]
) || classmethod_decorators
.iter()
.any(|decorator| call_path == CallPath::from_qualified_name(decorator))
.any(|decorator| qualified_name == QualifiedName::from_dotted_name(decorator))
})
{
return true;
@ -115,8 +115,8 @@ fn is_class_method(
// We do not have a resolvable call path, most likely from a decorator like
// `@someproperty.setter`. Instead, match on the last element.
if !classmethod_decorators.is_empty() {
if CallPath::from_expr(decorator).is_some_and(|call_path| {
call_path.segments().last().is_some_and(|tail| {
if UnqualifiedName::from_expr(decorator).is_some_and(|name| {
name.segments().last().is_some_and(|tail| {
classmethod_decorators
.iter()
.any(|decorator| tail == decorator)

View file

@ -17,10 +17,10 @@ pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool {
return false;
};
semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
[
"sys",
"path",
@ -47,10 +47,10 @@ pub fn is_os_environ_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool
match stmt {
Stmt::Expr(ast::StmtExpr { value, .. }) => match value.as_ref() {
Expr::Call(ast::ExprCall { func, .. }) => semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
["os", "putenv" | "unsetenv"]
| [
"os",
@ -63,20 +63,24 @@ pub fn is_os_environ_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool
},
Stmt::Delete(ast::StmtDelete { targets, .. }) => targets.iter().any(|target| {
semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"]))
.resolve_qualified_name(map_subscript(target))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["os", "environ"])
})
}),
Stmt::Assign(ast::StmtAssign { targets, .. }) => targets.iter().any(|target| {
semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"]))
.resolve_qualified_name(map_subscript(target))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["os", "environ"])
})
}),
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"])),
.resolve_qualified_name(map_subscript(target))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "environ"])),
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"])),
.resolve_qualified_name(map_subscript(target))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "environ"])),
_ => false,
}
}
@ -95,6 +99,6 @@ pub fn is_matplotlib_activation(stmt: &Stmt, semantic: &SemanticModel) -> bool {
return false;
};
semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| matches!(call_path.segments(), ["matplotlib", "use"]))
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["matplotlib", "use"]))
}

View file

@ -1,5 +1,5 @@
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword};
use crate::model::SemanticModel;
@ -27,9 +27,9 @@ pub fn is_logger_candidate(
// If the symbol was imported from another module, ensure that it's either a user-specified
// logger object, the `logging` module itself, or `flask.current_app.logger`.
if let Some(call_path) = semantic.resolve_call_path(value) {
if let Some(qualified_name) = semantic.resolve_qualified_name(value) {
if matches!(
call_path.segments(),
qualified_name.segments(),
["logging"] | ["flask", "current_app", "logger"]
) {
return true;
@ -37,7 +37,7 @@ pub fn is_logger_candidate(
if logger_objects
.iter()
.any(|logger| CallPath::from_qualified_name(logger) == call_path)
.any(|logger| QualifiedName::from_dotted_name(logger) == qualified_name)
{
return true;
}
@ -47,8 +47,8 @@ pub fn is_logger_candidate(
// Otherwise, if the symbol was defined in the current module, match against some common
// logger names.
if let Some(call_path) = CallPath::from_expr(value) {
if let Some(tail) = call_path.segments().last() {
if let Some(name) = UnqualifiedName::from_expr(value) {
if let Some(tail) = name.segments().last() {
if tail.starts_with("log")
|| tail.ends_with("logger")
|| tail.ends_with("logging")
@ -78,8 +78,8 @@ pub fn exc_info<'a>(arguments: &'a Arguments, semantic: &SemanticModel) -> Optio
if exc_info
.value
.as_call_expr()
.and_then(|call| semantic.resolve_call_path(&call.func))
.is_some_and(|call_path| matches!(call_path.segments(), ["sys", "exc_info"]))
.and_then(|call| semantic.resolve_qualified_name(&call.func))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "exc_info"]))
{
return Some(exc_info);
}

View file

@ -1,7 +1,7 @@
//! Analysis rules for the `typing` module.
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::{any_over_expr, is_const_false, map_subscript};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr, Int, Operator, ParameterWithDefault, Parameters, Stmt};
use ruff_python_stdlib::typing::{
as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type,
@ -42,43 +42,45 @@ pub fn match_annotated_subscript<'a>(
typing_modules: impl Iterator<Item = &'a str>,
extend_generics: &[String],
) -> Option<SubscriptKind> {
semantic.resolve_call_path(expr).and_then(|call_path| {
if is_standard_library_literal(call_path.segments()) {
return Some(SubscriptKind::Literal);
}
semantic
.resolve_qualified_name(expr)
.and_then(|qualified_name| {
if is_standard_library_literal(qualified_name.segments()) {
return Some(SubscriptKind::Literal);
}
if is_standard_library_generic(call_path.segments())
|| extend_generics
.iter()
.map(|target| CallPath::from_qualified_name(target))
.any(|target| call_path == target)
{
return Some(SubscriptKind::Generic);
}
if is_standard_library_generic(qualified_name.segments())
|| extend_generics
.iter()
.map(|target| QualifiedName::from_dotted_name(target))
.any(|target| qualified_name == target)
{
return Some(SubscriptKind::Generic);
}
if is_pep_593_generic_type(call_path.segments()) {
return Some(SubscriptKind::PEP593Annotation);
}
if is_pep_593_generic_type(qualified_name.segments()) {
return Some(SubscriptKind::PEP593Annotation);
}
for module in typing_modules {
let module_call_path: CallPath = CallPath::from_unqualified_name(module);
if call_path.starts_with(&module_call_path) {
if let Some(member) = call_path.segments().last() {
if is_literal_member(member) {
return Some(SubscriptKind::Literal);
}
if is_standard_library_generic_member(member) {
return Some(SubscriptKind::Generic);
}
if is_pep_593_generic_member(member) {
return Some(SubscriptKind::PEP593Annotation);
for module in typing_modules {
let module_qualified_name = QualifiedName::imported(module);
if qualified_name.starts_with(&module_qualified_name) {
if let Some(member) = qualified_name.segments().last() {
if is_literal_member(member) {
return Some(SubscriptKind::Literal);
}
if is_standard_library_generic_member(member) {
return Some(SubscriptKind::Generic);
}
if is_pep_593_generic_member(member) {
return Some(SubscriptKind::PEP593Annotation);
}
}
}
}
}
None
})
None
})
}
#[derive(Debug, Clone, Eq, PartialEq)]
@ -103,10 +105,10 @@ impl std::fmt::Display for ModuleMember {
pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<ModuleMember> {
semantic
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
.then(|| semantic.resolve_call_path(expr))
.then(|| semantic.resolve_qualified_name(expr))
.flatten()
.and_then(|call_path| {
let [module, member] = call_path.segments() else {
.and_then(|qualified_name| {
let [module, member] = qualified_name.segments() else {
return None;
};
as_pep_585_generic(module, member).map(|(module, member)| {
@ -121,12 +123,14 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<Module
/// Return whether a given expression uses a PEP 585 standard library generic.
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
let [module, name] = call_path.segments() else {
return false;
};
has_pep_585_generic(module, name)
})
semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
let [module, name] = qualified_name.segments() else {
return false;
};
has_pep_585_generic(module, name)
})
}
#[derive(Debug, Copy, Clone)]
@ -195,12 +199,12 @@ pub fn to_pep604_operator(
}
semantic
.resolve_call_path(value)
.resolve_qualified_name(value)
.as_ref()
.and_then(|call_path| {
if semantic.match_typing_call_path(call_path, "Optional") {
.and_then(|qualified_name| {
if semantic.match_typing_qualified_name(qualified_name, "Optional") {
Some(Pep604Operator::Optional)
} else if semantic.match_typing_call_path(call_path, "Union") {
} else if semantic.match_typing_qualified_name(qualified_name, "Union") {
Some(Pep604Operator::Union)
} else {
None
@ -213,23 +217,26 @@ pub fn to_pep604_operator(
pub fn is_immutable_annotation(
expr: &Expr,
semantic: &SemanticModel,
extend_immutable_calls: &[CallPath],
extend_immutable_calls: &[QualifiedName],
) -> bool {
match expr {
Expr::Name(_) | Expr::Attribute(_) => {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
is_immutable_non_generic_type(call_path.segments())
|| is_immutable_generic_type(call_path.segments())
|| extend_immutable_calls
.iter()
.any(|target| call_path == *target)
})
semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
is_immutable_non_generic_type(qualified_name.segments())
|| is_immutable_generic_type(qualified_name.segments())
|| extend_immutable_calls
.iter()
.any(|target| qualified_name == *target)
})
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
semantic.resolve_call_path(value).is_some_and(|call_path| {
if is_immutable_generic_type(call_path.segments()) {
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic
.resolve_qualified_name(value)
.is_some_and(|qualified_name| {
if is_immutable_generic_type(qualified_name.segments()) {
true
} else if matches!(call_path.segments(), ["typing", "Union"]) {
} else if matches!(qualified_name.segments(), ["typing", "Union"]) {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
elts.iter().all(|elt| {
is_immutable_annotation(elt, semantic, extend_immutable_calls)
@ -237,9 +244,9 @@ pub fn is_immutable_annotation(
} else {
false
}
} else if matches!(call_path.segments(), ["typing", "Optional"]) {
} else if matches!(qualified_name.segments(), ["typing", "Optional"]) {
is_immutable_annotation(slice, semantic, extend_immutable_calls)
} else if is_pep_593_generic_type(call_path.segments()) {
} else if is_pep_593_generic_type(qualified_name.segments()) {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
elts.first().is_some_and(|elt| {
is_immutable_annotation(elt, semantic, extend_immutable_calls)
@ -250,8 +257,7 @@ pub fn is_immutable_annotation(
} else {
false
}
})
}
}),
Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::BitOr,
@ -270,22 +276,24 @@ pub fn is_immutable_annotation(
pub fn is_immutable_func(
func: &Expr,
semantic: &SemanticModel,
extend_immutable_calls: &[CallPath],
extend_immutable_calls: &[QualifiedName],
) -> bool {
semantic.resolve_call_path(func).is_some_and(|call_path| {
is_immutable_return_type(call_path.segments())
|| extend_immutable_calls
.iter()
.any(|target| call_path == *target)
})
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
is_immutable_return_type(qualified_name.segments())
|| extend_immutable_calls
.iter()
.any(|target| qualified_name == *target)
})
}
/// Return `true` if `func` is a function that returns a mutable value.
pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(func)
.resolve_qualified_name(func)
.as_ref()
.map(CallPath::segments)
.map(QualifiedName::segments)
.is_some_and(is_mutable_return_type)
}
@ -336,9 +344,14 @@ pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> boo
let ast::StmtIf { test, .. } = stmt;
any_over_expr(test, &|expr| {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
matches!(call_path.segments(), ["sys", "version_info" | "platform"])
})
semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["sys", "version_info" | "platform"]
)
})
})
}
@ -608,19 +621,19 @@ pub struct IoBaseChecker;
impl TypeChecker for IoBaseChecker {
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(annotation)
.is_some_and(|call_path| {
if semantic.match_typing_call_path(&call_path, "IO") {
.resolve_qualified_name(annotation)
.is_some_and(|qualified_name| {
if semantic.match_typing_qualified_name(&qualified_name, "IO") {
return true;
}
if semantic.match_typing_call_path(&call_path, "BinaryIO") {
if semantic.match_typing_qualified_name(&qualified_name, "BinaryIO") {
return true;
}
if semantic.match_typing_call_path(&call_path, "TextIO") {
if semantic.match_typing_qualified_name(&qualified_name, "TextIO") {
return true;
}
matches!(
call_path.segments(),
qualified_name.segments(),
[
"io",
"IOBase"
@ -652,25 +665,27 @@ impl TypeChecker for IoBaseChecker {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
if attr.as_str() == "open" {
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
return semantic.resolve_call_path(func).is_some_and(|call_path| {
matches!(
call_path.segments(),
[
"pathlib",
"Path" | "PurePath" | "PurePosixPath" | "PureWindowsPath"
]
)
});
return semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
[
"pathlib",
"Path" | "PurePath" | "PurePosixPath" | "PureWindowsPath"
]
)
});
}
}
}
// Ex) `open("file.txt")`
semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
["io", "open" | "open_code"] | ["os" | "", "open"]
)
})
@ -736,7 +751,7 @@ fn find_parameter<'a>(
.find(|arg| arg.parameter.name.range() == binding.range())
}
/// Return the [`CallPath`] of the value to which the given [`Expr`] is assigned, if any.
/// Return the [`QualifiedName`] of the value to which the given [`Expr`] is assigned, if any.
///
/// For example, given:
/// ```python
@ -750,20 +765,20 @@ fn find_parameter<'a>(
pub fn resolve_assignment<'a>(
expr: &'a Expr,
semantic: &'a SemanticModel<'a>,
) -> Option<CallPath<'a>> {
) -> Option<QualifiedName<'a>> {
let name = expr.as_name_expr()?;
let binding_id = semantic.resolve_name(name)?;
let statement = semantic.binding(binding_id).statement(semantic)?;
match statement {
Stmt::Assign(ast::StmtAssign { value, .. }) => {
let ast::ExprCall { func, .. } = value.as_call_expr()?;
semantic.resolve_call_path(func)
semantic.resolve_qualified_name(func)
}
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => {
let ast::ExprCall { func, .. } = value.as_call_expr()?;
semantic.resolve_call_path(func)
semantic.resolve_qualified_name(func)
}
_ => None,
}

View file

@ -2,8 +2,8 @@ use std::path::Path;
use ruff_python_ast::{self as ast, Decorator};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
use crate::model::SemanticModel;
@ -17,8 +17,8 @@ pub enum Visibility {
pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| matches!(call_path.segments(), ["", "staticmethod"]))
.resolve_qualified_name(map_callable(&decorator.expression))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "staticmethod"]))
})
}
@ -26,8 +26,8 @@ pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -
pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| matches!(call_path.segments(), ["", "classmethod"]))
.resolve_qualified_name(map_callable(&decorator.expression))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "classmethod"]))
})
}
@ -49,10 +49,10 @@ pub fn is_override(decorator_list: &[Decorator], semantic: &SemanticModel) -> bo
pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
.resolve_qualified_name(map_callable(&decorator.expression))
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
[
"abc",
"abstractmethod"
@ -70,19 +70,19 @@ pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bo
/// `@property`-like decorators.
pub fn is_property(
decorator_list: &[Decorator],
extra_properties: &[CallPath],
extra_properties: &[QualifiedName],
semantic: &SemanticModel,
) -> bool {
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
.resolve_qualified_name(map_callable(&decorator.expression))
.is_some_and(|qualified_name| {
matches!(
call_path.segments(),
qualified_name.segments(),
["", "property"] | ["functools", "cached_property"]
) || extra_properties
.iter()
.any(|extra_property| extra_property.segments() == call_path.segments())
.any(|extra_property| extra_property.segments() == qualified_name.segments())
})
})
}
@ -187,9 +187,9 @@ pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility
pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
// Is this a setter or deleter?
if function.decorator_list.iter().any(|decorator| {
CallPath::from_expr(&decorator.expression).is_some_and(|call_path| {
call_path.segments() == [function.name.as_str(), "setter"]
|| call_path.segments() == [function.name.as_str(), "deleter"]
UnqualifiedName::from_expr(&decorator.expression).is_some_and(|name| {
name.segments() == [function.name.as_str(), "setter"]
|| name.segments() == [function.name.as_str(), "deleter"]
})
}) {
return Visibility::Private;