mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:37 +00:00
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:
parent
ba4328226d
commit
a6d892b1f4
181 changed files with 1692 additions and 1412 deletions
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue