diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 101cda7b6bfbd..0ed3800a59c93 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -418,11 +418,11 @@ impl<'a> Visitor<'a> for Checker<'a> { self.semantic.add_module(module); if alias.asname.is_none() && alias.name.contains('.') { - let call_path: Box<[&str]> = alias.name.split('.').collect(); + let qualified_name: Box<[&str]> = alias.name.split('.').collect(); self.add_binding( module, alias.identifier(), - BindingKind::SubmoduleImport(SubmoduleImport { call_path }), + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }), BindingFlags::EXTERNAL, ); } else { @@ -439,11 +439,11 @@ impl<'a> Visitor<'a> for Checker<'a> { } let name = alias.asname.as_ref().unwrap_or(&alias.name); - let call_path: Box<[&str]> = alias.name.split('.').collect(); + let qualified_name: Box<[&str]> = alias.name.split('.').collect(); self.add_binding( name, alias.identifier(), - BindingKind::Import(Import { call_path }), + BindingKind::Import(Import { qualified_name }), flags, ); } @@ -503,12 +503,12 @@ impl<'a> Visitor<'a> for Checker<'a> { // Attempt to resolve any relative imports; but if we don't know the current // module path, or the relative import extends beyond the package root, // fallback to a literal representation (e.g., `[".", "foo"]`). - let call_path = collect_import_from_member(level, module, &alias.name) + let qualified_name = collect_import_from_member(level, module, &alias.name) .into_boxed_slice(); self.add_binding( name, alias.identifier(), - BindingKind::FromImport(FromImport { call_path }), + BindingKind::FromImport(FromImport { qualified_name }), flags, ); } @@ -751,8 +751,8 @@ impl<'a> Visitor<'a> for Checker<'a> { }) => { let mut handled_exceptions = Exceptions::empty(); for type_ in extract_handled_exceptions(handlers) { - if let Some(call_path) = self.semantic.resolve_call_path(type_) { - match call_path.segments() { + if let Some(qualified_name) = self.semantic.resolve_qualified_name(type_) { + match qualified_name.segments() { ["", "NameError"] => { handled_exceptions |= Exceptions::NAME_ERROR; } @@ -1065,42 +1065,54 @@ impl<'a> Visitor<'a> for Checker<'a> { }) => { self.visit_expr(func); - let callable = self.semantic.resolve_call_path(func).and_then(|call_path| { - if self.semantic.match_typing_call_path(&call_path, "cast") { - Some(typing::Callable::Cast) - } else if self.semantic.match_typing_call_path(&call_path, "NewType") { - Some(typing::Callable::NewType) - } else if self.semantic.match_typing_call_path(&call_path, "TypeVar") { - Some(typing::Callable::TypeVar) - } else if self - .semantic - .match_typing_call_path(&call_path, "NamedTuple") - { - Some(typing::Callable::NamedTuple) - } else if self - .semantic - .match_typing_call_path(&call_path, "TypedDict") - { - Some(typing::Callable::TypedDict) - } else if matches!( - call_path.segments(), - [ - "mypy_extensions", - "Arg" - | "DefaultArg" - | "NamedArg" - | "DefaultNamedArg" - | "VarArg" - | "KwArg" - ] - ) { - Some(typing::Callable::MypyExtension) - } else if matches!(call_path.segments(), ["", "bool"]) { - Some(typing::Callable::Bool) - } else { - None - } - }); + let callable = + self.semantic + .resolve_qualified_name(func) + .and_then(|qualified_name| { + if self + .semantic + .match_typing_qualified_name(&qualified_name, "cast") + { + Some(typing::Callable::Cast) + } else if self + .semantic + .match_typing_qualified_name(&qualified_name, "NewType") + { + Some(typing::Callable::NewType) + } else if self + .semantic + .match_typing_qualified_name(&qualified_name, "TypeVar") + { + Some(typing::Callable::TypeVar) + } else if self + .semantic + .match_typing_qualified_name(&qualified_name, "NamedTuple") + { + Some(typing::Callable::NamedTuple) + } else if self + .semantic + .match_typing_qualified_name(&qualified_name, "TypedDict") + { + Some(typing::Callable::TypedDict) + } else if matches!( + qualified_name.segments(), + [ + "mypy_extensions", + "Arg" + | "DefaultArg" + | "NamedArg" + | "DefaultNamedArg" + | "VarArg" + | "KwArg" + ] + ) { + Some(typing::Callable::MypyExtension) + } else if matches!(qualified_name.segments(), ["", "bool"]) { + Some(typing::Callable::Bool) + } else { + None + } + }); match callable { Some(typing::Callable::Bool) => { let mut args = arguments.args.iter(); diff --git a/crates/ruff_linter/src/cst/helpers.rs b/crates/ruff_linter/src/cst/helpers.rs index e32c6ab887cb3..731ff8a56d2ac 100644 --- a/crates/ruff_linter/src/cst/helpers.rs +++ b/crates/ruff_linter/src/cst/helpers.rs @@ -1,44 +1,7 @@ use libcst_native::{ - Expression, Name, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace, UnaryOperation, + Expression, Name, ParenthesizableWhitespace, SimpleWhitespace, UnaryOperation, }; -fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) { - match expr { - Expression::Call(expr) => { - compose_call_path_inner(&expr.func, parts); - } - Expression::Attribute(expr) => { - compose_call_path_inner(&expr.value, parts); - parts.push(expr.attr.value); - } - Expression::Name(expr) => { - parts.push(expr.value); - } - _ => {} - } -} - -pub(crate) fn compose_call_path(expr: &Expression) -> Option { - let mut segments = vec![]; - compose_call_path_inner(expr, &mut segments); - if segments.is_empty() { - None - } else { - Some(segments.join(".")) - } -} - -pub(crate) fn compose_module_path(module: &NameOrAttribute) -> String { - match module { - NameOrAttribute::N(name) => name.value.to_string(), - NameOrAttribute::A(attr) => { - let name = attr.attr.value; - let prefix = compose_call_path(&attr.value); - prefix.map_or_else(|| name.to_string(), |prefix| format!("{prefix}.{name}")) - } - } -} - /// Return a [`ParenthesizableWhitespace`] containing a single space. pub(crate) fn space() -> ParenthesizableWhitespace<'static> { ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")) diff --git a/crates/ruff_linter/src/fix/codemods.rs b/crates/ruff_linter/src/fix/codemods.rs index cf57f2126117b..c3c7691726967 100644 --- a/crates/ruff_linter/src/fix/codemods.rs +++ b/crates/ruff_linter/src/fix/codemods.rs @@ -2,14 +2,16 @@ //! and return the modified code snippet as output. use anyhow::{bail, Result}; use libcst_native::{ - Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement, + Codegen, CodegenState, Expression, ImportNames, NameOrAttribute, ParenthesizableWhitespace, + SmallStatement, Statement, }; +use ruff_python_ast::name::UnqualifiedName; +use smallvec::{smallvec, SmallVec}; use ruff_python_ast::Stmt; use ruff_python_codegen::Stylist; use ruff_source_file::Locator; -use crate::cst::helpers::compose_module_path; use crate::cst::matchers::match_statement; /// Glue code to make libcst codegen work with ruff's Stylist @@ -78,7 +80,7 @@ pub(crate) fn remove_imports<'a>( for member in member_names { let alias_index = aliases .iter() - .position(|alias| member == compose_module_path(&alias.name)); + .position(|alias| member == qualified_name_from_name_or_attribute(&alias.name)); if let Some(index) = alias_index { aliases.remove(index); } @@ -142,7 +144,7 @@ pub(crate) fn retain_imports( aliases.retain(|alias| { member_names .iter() - .any(|member| *member == compose_module_path(&alias.name)) + .any(|member| *member == qualified_name_from_name_or_attribute(&alias.name)) }); // But avoid destroying any trailing comments. @@ -164,3 +166,40 @@ pub(crate) fn retain_imports( Ok(tree.codegen_stylist(stylist)) } + +fn collect_segments<'a>(expr: &'a Expression, parts: &mut SmallVec<[&'a str; 8]>) { + match expr { + Expression::Call(expr) => { + collect_segments(&expr.func, parts); + } + Expression::Attribute(expr) => { + collect_segments(&expr.value, parts); + parts.push(expr.attr.value); + } + Expression::Name(expr) => { + parts.push(expr.value); + } + _ => {} + } +} + +fn unqualified_name_from_expression<'a>(expr: &'a Expression<'a>) -> Option> { + let mut segments = smallvec![]; + collect_segments(expr, &mut segments); + if segments.is_empty() { + None + } else { + Some(segments.into_iter().collect()) + } +} + +fn qualified_name_from_name_or_attribute(module: &NameOrAttribute) -> String { + match module { + NameOrAttribute::N(name) => name.value.to_string(), + NameOrAttribute::A(attr) => { + let name = attr.attr.value; + let prefix = unqualified_name_from_expression(&attr.value); + prefix.map_or_else(|| name.to_string(), |prefix| format!("{prefix}.{name}")) + } + } +} diff --git a/crates/ruff_linter/src/renamer.rs b/crates/ruff_linter/src/renamer.rs index 56fb1e6ad0b8c..4fa303d462ab9 100644 --- a/crates/ruff_linter/src/renamer.rs +++ b/crates/ruff_linter/src/renamer.rs @@ -232,7 +232,7 @@ impl Renamer { } BindingKind::SubmoduleImport(import) => { // Ex) Rename `import pandas.core` to `import pandas as pd`. - let module_name = import.call_path.first().unwrap(); + let module_name = import.qualified_name.first().unwrap(); Some(Edit::range_replacement( format!("{module_name} as {target}"), binding.range(), diff --git a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs index 7f90283f9995a..870525d9ad5de 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/task_variable_name.rs @@ -68,8 +68,8 @@ pub(crate) fn variable_name_task_id( // If the function doesn't come from Airflow, we can't do anything. if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments()[0], "airflow")) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments()[0], "airflow")) { return None; } diff --git a/crates/ruff_linter/src/rules/flake8_2020/helpers.rs b/crates/ruff_linter/src/rules/flake8_2020/helpers.rs index 4e0fdad7d6e24..a838ab333e441 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/helpers.rs @@ -4,6 +4,6 @@ use ruff_python_semantic::SemanticModel; pub(super) fn is_sys(expr: &Expr, target: &str, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(expr) - .is_some_and(|call_path| call_path.segments() == ["sys", target]) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| qualified_name.segments() == ["sys", target]) } diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs index 433f6a2b62677..ad3d40c9069e1 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -53,8 +53,8 @@ pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["six", "PY3"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["six", "PY3"])) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs index 01b7be8085324..1318d4cbd705b 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_http_call.rs @@ -2,7 +2,7 @@ use ruff_python_ast::ExprCall; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -41,9 +41,9 @@ impl Violation for BlockingHttpCallInAsyncFunction { } } -fn is_blocking_http_call(call_path: &CallPath) -> bool { +fn is_blocking_http_call(qualified_name: &QualifiedName) -> bool { matches!( - call_path.segments(), + qualified_name.segments(), ["urllib", "request", "urlopen"] | [ "httpx" | "requests", @@ -65,7 +65,7 @@ pub(crate) fn blocking_http_call(checker: &mut Checker, call: &ExprCall) { if checker.semantic().in_async_context() { if checker .semantic() - .resolve_call_path(call.func.as_ref()) + .resolve_qualified_name(call.func.as_ref()) .as_ref() .is_some_and(is_blocking_http_call) { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_os_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_os_call.rs index db0d82ea2dc89..59848aeb7d040 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_os_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_os_call.rs @@ -2,7 +2,7 @@ use ruff_python_ast::ExprCall; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; @@ -47,7 +47,7 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) { if checker.semantic().in_async_context() { if checker .semantic() - .resolve_call_path(call.func.as_ref()) + .resolve_qualified_name(call.func.as_ref()) .as_ref() .is_some_and(is_unsafe_os_method) { @@ -60,9 +60,9 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) { } } -fn is_unsafe_os_method(call_path: &CallPath) -> bool { +fn is_unsafe_os_method(qualified_name: &QualifiedName) -> bool { matches!( - call_path.segments(), + qualified_name.segments(), [ "os", "popen" diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs index ae3f98f83bedc..f85383e6468fd 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/open_sleep_or_subprocess_call.rs @@ -58,24 +58,26 @@ pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, call: &ast::E /// Returns `true` if the expression resolves to a blocking call, like `time.sleep` or /// `subprocess.run`. fn is_open_sleep_or_subprocess_call(func: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!( - call_path.segments(), - ["", "open"] - | ["time", "sleep"] - | [ - "subprocess", - "run" - | "Popen" - | "call" - | "check_call" - | "check_output" - | "getoutput" - | "getstatusoutput" - ] - | ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] - ) - }) + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "open"] + | ["time", "sleep"] + | [ + "subprocess", + "run" + | "Popen" + | "call" + | "check_call" + | "check_output" + | "getoutput" + | "getstatusoutput" + ] + | ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"] + ) + }) } /// Returns `true` if an expression resolves to a call to `pathlib.Path.open`. @@ -94,10 +96,10 @@ fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool { // Path("foo").open() // ``` if let Expr::Call(call) = value.as_ref() { - let Some(call_path) = semantic.resolve_call_path(call.func.as_ref()) else { + let Some(qualified_name) = semantic.resolve_qualified_name(call.func.as_ref()) else { return false; }; - if call_path.segments() == ["pathlib", "Path"] { + if qualified_name.segments() == ["pathlib", "Path"] { return true; } } @@ -123,6 +125,6 @@ fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool { }; semantic - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| call_path.segments() == ["pathlib", "Path"]) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| qualified_name.segments() == ["pathlib", "Path"]) } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/helpers.rs b/crates/ruff_linter/src/rules/flake8_bandit/helpers.rs index 1f16f124c008e..53c7e2c039433 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/helpers.rs @@ -23,14 +23,24 @@ pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticMode type_.map_or(true, |type_| { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ { elts.iter().any(|type_| { - semantic.resolve_call_path(type_).is_some_and(|call_path| { - matches!(call_path.segments(), ["", "Exception" | "BaseException"]) - }) + semantic + .resolve_qualified_name(type_) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "Exception" | "BaseException"] + ) + }) }) } else { - semantic.resolve_call_path(type_).is_some_and(|call_path| { - matches!(call_path.segments(), ["", "Exception" | "BaseException"]) - }) + semantic + .resolve_qualified_name(type_) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "Exception" | "BaseException"] + ) + }) } }) } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs index c65e3b75b3d31..bff5c20674686 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/bad_file_permissions.rs @@ -2,7 +2,7 @@ use anyhow::Result; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; @@ -66,8 +66,8 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["os", "chmod"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "chmod"])) { if let Some(mode_arg) = call.arguments.find_argument("mode", 1) { match parse_mask(mode_arg, checker.semantic()) { @@ -101,8 +101,8 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) const WRITE_WORLD: u16 = 0o2; const EXECUTE_GROUP: u16 = 0o10; -fn py_stat(call_path: &CallPath) -> Option { - match call_path.segments() { +fn py_stat(qualified_name: &QualifiedName) -> Option { + match qualified_name.segments() { ["stat", "ST_MODE"] => Some(0o0), ["stat", "S_IFDOOR"] => Some(0o0), ["stat", "S_IFPORT"] => Some(0o0), @@ -155,7 +155,10 @@ fn parse_mask(expr: &Expr, semantic: &SemanticModel) -> Result> { Some(value) => Ok(Some(value)), None => anyhow::bail!("int value out of range"), }, - Expr::Attribute(_) => Ok(semantic.resolve_call_path(expr).as_ref().and_then(py_stat)), + Expr::Attribute(_) => Ok(semantic + .resolve_qualified_name(expr) + .as_ref() + .and_then(py_stat)), Expr::BinOp(ast::ExprBinOp { left, op, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs index 4ab3a2d62234e..577accaa36f25 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/django_raw_sql.rs @@ -42,10 +42,10 @@ pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["django", "db", "models", "expressions", "RawSQL"] ) }) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs index d1680b356c7f9..83eae0aec6ce0 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/exec_used.rs @@ -35,8 +35,8 @@ impl Violation for ExecBuiltin { pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["" | "builtin", "exec"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["" | "builtin", "exec"])) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs index 3c3908f0bc8f5..db3e3adf34c8b 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/flask_debug_true.rs @@ -65,7 +65,7 @@ pub(crate) fn flask_debug_true(checker: &mut Checker, call: &ExprCall) { } if typing::resolve_assignment(value, checker.semantic()) - .is_some_and(|call_path| matches!(call_path.segments(), ["flask", "Flask"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["flask", "Flask"])) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 291387bc70a46..fc74174fe32be 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -74,8 +74,8 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike) { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["tempfile", ..])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["tempfile", ..])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs index 8e0b1e64929d7..38520e804bd74 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hashlib_insecure_hash_functions.rs @@ -61,18 +61,17 @@ impl Violation for HashlibInsecureHashFunction { /// S324 pub(crate) fn hashlib_insecure_hash_functions(checker: &mut Checker, call: &ast::ExprCall) { - if let Some(hashlib_call) = - checker - .semantic() - .resolve_call_path(&call.func) - .and_then(|call_path| match call_path.segments() { - ["hashlib", "new"] => Some(HashlibCall::New), - ["hashlib", "md4"] => Some(HashlibCall::WeakHash("md4")), - ["hashlib", "md5"] => Some(HashlibCall::WeakHash("md5")), - ["hashlib", "sha"] => Some(HashlibCall::WeakHash("sha")), - ["hashlib", "sha1"] => Some(HashlibCall::WeakHash("sha1")), - _ => None, - }) + if let Some(hashlib_call) = checker + .semantic() + .resolve_qualified_name(&call.func) + .and_then(|qualified_name| match qualified_name.segments() { + ["hashlib", "new"] => Some(HashlibCall::New), + ["hashlib", "md4"] => Some(HashlibCall::WeakHash("md4")), + ["hashlib", "md5"] => Some(HashlibCall::WeakHash("md5")), + ["hashlib", "sha"] => Some(HashlibCall::WeakHash("sha")), + ["hashlib", "sha1"] => Some(HashlibCall::WeakHash("sha1")), + _ => None, + }) { if !is_used_for_security(&call.arguments) { return; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs index 1e6beac11ae95..128d0c9c1c8c4 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/jinja2_autoescape_false.rs @@ -61,8 +61,10 @@ impl Violation for Jinja2AutoescapeFalse { pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["jinja2", "Environment"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualifieed_name| { + matches!(qualifieed_name.segments(), ["jinja2", "Environment"]) + }) { if let Some(keyword) = call.arguments.find_keyword("autoescape") { match &keyword.value { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs index 77334b2abec7b..d92eb3466dfdf 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/logging_config_insecure_listen.rs @@ -42,8 +42,10 @@ pub(crate) fn logging_config_insecure_listen(checker: &mut Checker, call: &ast:: if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["logging", "config", "listen"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["logging", "config", "listen"]) + }) { if call.arguments.find_keyword("verify").is_some() { return; diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs index 186f8d96c6fac..d542487cc74bd 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs @@ -47,8 +47,10 @@ impl Violation for MakoTemplates { pub(crate) fn mako_templates(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["mako", "template", "Template"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["mako", "template", "Template"]) + }) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs index 0b6a7e530e5b3..8cf2cd3870ca6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/paramiko_calls.rs @@ -39,8 +39,10 @@ impl Violation for ParamikoCall { pub(crate) fn paramiko_call(checker: &mut Checker, func: &Expr) { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["paramiko", "exec_command"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["paramiko", "exec_command"]) + }) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index 295bec1d802da..463a73376be7f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -49,8 +49,8 @@ impl Violation for RequestWithNoCertValidation { pub(crate) fn request_with_no_cert_validation(checker: &mut Checker, call: &ast::ExprCall) { if let Some(target) = checker .semantic() - .resolve_call_path(&call.func) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(&call.func) + .and_then(|qualified_name| match qualified_name.segments() { ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => { Some("requests") } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs index 0af9c9316210e..26152e9ef76b2 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -52,10 +52,10 @@ impl Violation for RequestWithoutTimeout { pub(crate) fn request_without_timeout(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs index 89d4a316b26cc..952c8b11f418e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/shell_injection.rs @@ -419,8 +419,8 @@ enum CallKind { /// Return the [`CallKind`] of the given function call. fn get_call_kind(func: &Expr, semantic: &SemanticModel) -> Option { semantic - .resolve_call_path(func) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(func) + .and_then(|qualified_name| match qualified_name.segments() { &[module, submodule] => match module { "os" => match submodule { "execl" | "execle" | "execlp" | "execlpe" | "execv" | "execve" | "execvp" diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 73325af06011e..0c34637456c0d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -44,9 +44,12 @@ impl Violation for SnmpInsecureVersion { pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["pysnmp", "hlapi", "CommunityData"]) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", "CommunityData"] + ) }) { if let Some(keyword) = call.arguments.find_keyword("mpModel") { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index cebbf94ce0342..73b72335179d1 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -45,9 +45,12 @@ pub(crate) fn snmp_weak_cryptography(checker: &mut Checker, call: &ast::ExprCall if call.arguments.len() < 3 { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["pysnmp", "hlapi", "UsmUserData"]) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", "UsmUserData"] + ) }) { checker diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs index 9e1f863fa1863..8084aa0676ec1 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssh_no_host_key_verification.rs @@ -60,10 +60,10 @@ pub(crate) fn ssh_no_host_key_verification(checker: &mut Checker, call: &ExprCal // Detect either, e.g., `paramiko.client.AutoAddPolicy` or `paramiko.client.AutoAddPolicy()`. if !checker .semantic() - .resolve_call_path(map_callable(policy_argument)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_callable(policy_argument)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["paramiko", "client", "AutoAddPolicy" | "WarningPolicy"] | ["paramiko", "AutoAddPolicy" | "WarningPolicy"] ) @@ -72,9 +72,9 @@ pub(crate) fn ssh_no_host_key_verification(checker: &mut Checker, call: &ExprCal return; } - if typing::resolve_assignment(value, checker.semantic()).is_some_and(|call_path| { + if typing::resolve_assignment(value, checker.semantic()).is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["paramiko", "client", "SSHClient"] | ["paramiko", "SSHClient"] ) }) { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs index 6fb26a83e5cc3..796c7979a3e29 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_insecure_version.rs @@ -51,8 +51,8 @@ impl Violation for SslInsecureVersion { pub(crate) fn ssl_insecure_version(checker: &mut Checker, call: &ExprCall) { let Some(keyword) = checker .semantic() - .resolve_call_path(call.func.as_ref()) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(call.func.as_ref()) + .and_then(|qualified_name| match qualified_name.segments() { ["ssl", "wrap_socket"] => Some("ssl_version"), ["OpenSSL", "SSL", "Context"] => Some("method"), _ => None, diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs index 221d102e13836..13514f95ce1cd 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/ssl_with_no_version.rs @@ -39,8 +39,8 @@ impl Violation for SslWithNoVersion { pub(crate) fn ssl_with_no_version(checker: &mut Checker, call: &ExprCall) { if checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["ssl", "wrap_socket"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["ssl", "wrap_socket"])) { if call.arguments.find_keyword("ssl_version").is_none() { checker diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index 143c982dda4e7..bcd7d305fe87d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -825,8 +825,8 @@ impl Violation for SuspiciousFTPLibUsage { /// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323 pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) { - let Some(diagnostic_kind) = checker.semantic().resolve_call_path(call.func.as_ref()).and_then(|call_path| { - match call_path.segments() { + let Some(diagnostic_kind) = checker.semantic().resolve_qualified_name(call.func.as_ref()).and_then(|qualified_name| { + match qualified_name.segments() { // Pickle ["pickle" | "dill", "load" | "loads" | "Unpickler"] | ["shelve", "open" | "DbfilenameShelf"] | @@ -906,9 +906,9 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) { pub(crate) fn suspicious_function_decorator(checker: &mut Checker, decorator: &Decorator) { let Some(diagnostic_kind) = checker .semantic() - .resolve_call_path(&decorator.expression) - .and_then(|call_path| { - match call_path.segments() { + .resolve_qualified_name(&decorator.expression) + .and_then(|qualified_name| { + match qualified_name.segments() { // MarkSafe ["django", "utils", "safestring" | "html", "mark_safe"] => { Some(SuspiciousMarkSafeUsage.into()) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs index 3b2b72b53b22b..f63e134b333f3 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_yaml_load.rs @@ -62,16 +62,16 @@ impl Violation for UnsafeYAMLLoad { pub(crate) fn unsafe_yaml_load(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["yaml", "load"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["yaml", "load"])) { if let Some(loader_arg) = call.arguments.find_argument("Loader", 1) { if !checker .semantic() - .resolve_call_path(loader_arg) - .is_some_and(|call_path| { + .resolve_qualified_name(loader_arg) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["yaml", "SafeLoader" | "CSafeLoader"] | ["yaml", "loader", "SafeLoader" | "CSafeLoader"] ) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs index d24e7c0c435a8..48137db7fb4d0 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/weak_cryptographic_key.rs @@ -101,8 +101,8 @@ fn extract_cryptographic_key( checker: &mut Checker, call: &ExprCall, ) -> Option<(CryptographicKey, TextRange)> { - let call_path = checker.semantic().resolve_call_path(&call.func)?; - match call_path.segments() { + let qualified_name = checker.semantic().resolve_qualified_name(&call.func)?; + match qualified_name.segments() { ["cryptography", "hazmat", "primitives", "asymmetric", function, "generate_private_key"] => { match *function { "dsa" => { @@ -116,9 +116,9 @@ fn extract_cryptographic_key( "ec" => { let argument = call.arguments.find_argument("curve", 0)?; let ExprAttribute { attr, value, .. } = argument.as_attribute_expr()?; - let call_path = checker.semantic().resolve_call_path(value)?; + let qualified_name = checker.semantic().resolve_qualified_name(value)?; if matches!( - call_path.segments(), + qualified_name.segments(), ["cryptography", "hazmat", "primitives", "asymmetric", "ec"] ) { Some(( diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index e15e93ddda56f..7d30d4da8577c 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -140,8 +140,8 @@ pub(crate) fn blind_except( Expr::Name(ast::ExprName { .. }) => { if checker .semantic() - .resolve_call_path(func.as_ref()) - .is_some_and(|call_path| match call_path.segments() { + .resolve_qualified_name(func.as_ref()) + .is_some_and(|qualified_name| match qualified_name.segments() { ["logging", "exception"] => true, ["logging", "error"] => { if let Some(keyword) = arguments.find_keyword("exc_info") { diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs index b144851070130..7977fc0d47ab8 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_default_value_positional_argument.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters}; use ruff_python_semantic::analyze::visibility; use ruff_text_size::Ranged; @@ -119,8 +119,8 @@ pub(crate) fn boolean_default_value_positional_argument( { // Allow Boolean defaults in setters. if decorator_list.iter().any(|decorator| { - CallPath::from_expr(&decorator.expression) - .is_some_and(|call_path| call_path.segments() == [name, "setter"]) + UnqualifiedName::from_expr(&decorator.expression) + .is_some_and(|unqualified_name| unqualified_name.segments() == [name, "setter"]) }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index 94c7ab0e0c586..81e0dc5dd0382 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -1,9 +1,8 @@ -use ruff_python_ast::{self as ast, Decorator, Expr, ParameterWithDefault, Parameters}; - use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; +use ruff_python_ast::{self as ast, Decorator, Expr, ParameterWithDefault, Parameters}; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::SemanticModel; use ruff_text_size::Ranged; @@ -136,8 +135,8 @@ pub(crate) fn boolean_type_hint_positional_argument( // Allow Boolean type hints in setters. if decorator_list.iter().any(|decorator| { - CallPath::from_expr(&decorator.expression) - .is_some_and(|call_path| call_path.segments() == [name, "setter"]) + UnqualifiedName::from_expr(&decorator.expression) + .is_some_and(|unqualified_name| unqualified_name.segments() == [name, "setter"]) }) { return; } @@ -196,11 +195,10 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) return false; } - let call_path = semantic.resolve_call_path(value); - if call_path - .as_ref() - .is_some_and(|call_path| semantic.match_typing_call_path(call_path, "Union")) - { + let qualified_name = semantic.resolve_qualified_name(value); + if qualified_name.as_ref().is_some_and(|qualified_name| { + semantic.match_typing_qualified_name(qualified_name, "Union") + }) { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { elts.iter() .any(|elt| match_annotation_to_complex_bool(elt, semantic)) @@ -208,10 +206,9 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) // Union with a single type is an invalid type annotation false } - } else if call_path - .as_ref() - .is_some_and(|call_path| semantic.match_typing_call_path(call_path, "Optional")) - { + } else if qualified_name.as_ref().is_some_and(|qualified_name| { + semantic.match_typing_qualified_name(qualified_name, "Optional") + }) { match_annotation_to_complex_bool(slice, semantic) } else { false diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs index dd265e11276b7..b7ec09e599be0 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -109,12 +109,14 @@ fn is_abc_class(bases: &[Expr], keywords: &[Keyword], semantic: &SemanticModel) keywords.iter().any(|keyword| { keyword.arg.as_ref().is_some_and(|arg| arg == "metaclass") && semantic - .resolve_call_path(&keyword.value) - .is_some_and(|call_path| matches!(call_path.segments(), ["abc", "ABCMeta"])) + .resolve_qualified_name(&keyword.value) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["abc", "ABCMeta"]) + }) }) || bases.iter().any(|base| { semantic - .resolve_call_path(base) - .is_some_and(|call_path| matches!(call_path.segments(), ["abc", "ABC"])) + .resolve_qualified_name(base) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["abc", "ABC"])) }) } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 993298409305a..ee2595ae283ea 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -95,14 +95,15 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) return; }; - let Some(exception) = checker - .semantic() - .resolve_call_path(arg) - .and_then(|call_path| match call_path.segments() { - ["", "Exception"] => Some(ExceptionKind::Exception), - ["", "BaseException"] => Some(ExceptionKind::BaseException), - _ => None, - }) + let Some(exception) = + checker + .semantic() + .resolve_qualified_name(arg) + .and_then(|qualified_name| match qualified_name.segments() { + ["", "Exception"] => Some(ExceptionKind::Exception), + ["", "BaseException"] => Some(ExceptionKind::BaseException), + _ => None, + }) else { return; }; @@ -112,8 +113,8 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) AssertionKind::AssertRaises } else if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "raises"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"])) && arguments.find_keyword("match").is_none() { AssertionKind::PytestRaises diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs index 5f8448c49f58d..016bc6bac5412 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs @@ -71,9 +71,14 @@ impl Violation for CachedInstanceMethod { } fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(expr).is_some_and(|call_path| { - matches!(call_path.segments(), ["functools", "lru_cache" | "cache"]) - }) + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["functools", "lru_cache" | "cache"] + ) + }) } /// B019 diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 947ff280c57d4..450253b18defc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -1,12 +1,12 @@ use itertools::Itertools; -use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext}; -use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use ruff_diagnostics::{AlwaysFixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; +use ruff_python_ast::{self as ast, ExceptHandler, Expr, ExprContext}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::pad; @@ -118,16 +118,16 @@ fn duplicate_handler_exceptions<'a>( checker: &mut Checker, expr: &'a Expr, elts: &'a [Expr], -) -> FxHashMap, &'a Expr> { - let mut seen: FxHashMap = FxHashMap::default(); - let mut duplicates: FxHashSet = FxHashSet::default(); +) -> FxHashMap, &'a Expr> { + let mut seen: FxHashMap = FxHashMap::default(); + let mut duplicates: FxHashSet = FxHashSet::default(); let mut unique_elts: Vec<&Expr> = Vec::default(); for type_ in elts { - if let Some(call_path) = CallPath::from_expr(type_) { - if seen.contains_key(&call_path) { - duplicates.insert(call_path); + if let Some(name) = UnqualifiedName::from_expr(type_) { + if seen.contains_key(&name) { + duplicates.insert(name); } else { - seen.entry(call_path).or_insert(type_); + seen.entry(name).or_insert(type_); unique_elts.push(type_); } } @@ -140,7 +140,7 @@ fn duplicate_handler_exceptions<'a>( DuplicateHandlerException { names: duplicates .into_iter() - .map(|call_path| call_path.segments().join(".")) + .map(|qualified_name| qualified_name.segments().join(".")) .sorted() .collect::>(), }, @@ -171,8 +171,8 @@ fn duplicate_handler_exceptions<'a>( /// B025 pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHandler]) { - let mut seen: FxHashSet = FxHashSet::default(); - let mut duplicates: FxHashMap> = FxHashMap::default(); + let mut seen: FxHashSet = FxHashSet::default(); + let mut duplicates: FxHashMap> = FxHashMap::default(); for handler in handlers { let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), @@ -183,11 +183,11 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand }; match type_.as_ref() { Expr::Attribute(_) | Expr::Name(_) => { - if let Some(call_path) = CallPath::from_expr(type_) { - if seen.contains(&call_path) { - duplicates.entry(call_path).or_default().push(type_); + if let Some(name) = UnqualifiedName::from_expr(type_) { + if seen.contains(&name) { + duplicates.entry(name).or_default().push(type_); } else { - seen.insert(call_path); + seen.insert(name); } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs index 53016ceb79255..dd677dcd69c33 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use ruff_diagnostics::Violation; use ruff_diagnostics::{Diagnostic, DiagnosticKind}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::{compose_call_path, CallPath}; +use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::typing::{ @@ -81,12 +81,15 @@ impl Violation for FunctionCallInDefaultArgument { struct ArgumentDefaultVisitor<'a, 'b> { semantic: &'a SemanticModel<'b>, - extend_immutable_calls: &'a [CallPath<'b>], + extend_immutable_calls: &'a [QualifiedName<'b>], diagnostics: Vec<(DiagnosticKind, TextRange)>, } impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> { - fn new(semantic: &'a SemanticModel<'b>, extend_immutable_calls: &'a [CallPath<'b>]) -> Self { + fn new( + semantic: &'a SemanticModel<'b>, + extend_immutable_calls: &'a [QualifiedName<'b>], + ) -> Self { Self { semantic, extend_immutable_calls, @@ -104,7 +107,7 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> { { self.diagnostics.push(( FunctionCallInDefaultArgument { - name: compose_call_path(func), + name: UnqualifiedName::from_expr(func).map(|name| name.to_string()), } .into(), expr.range(), @@ -123,12 +126,12 @@ impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> { /// B008 pub(crate) fn function_call_in_argument_default(checker: &mut Checker, parameters: &Parameters) { // Map immutable calls to (module, member) format. - let extend_immutable_calls: Vec = checker + let extend_immutable_calls: Vec = checker .settings .flake8_bugbear .extend_immutable_calls .iter() - .map(|target| CallPath::from_qualified_name(target)) + .map(|target| QualifiedName::from_dotted_name(target)) .collect(); let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index 1b84980cf8457..39bcbad883ee9 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -1,7 +1,7 @@ -use ast::call_path::CallPath; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_docstring_stmt; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault, Stmt}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_index::Indexer; @@ -98,12 +98,12 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast continue; }; - let extend_immutable_calls: Vec = checker + let extend_immutable_calls: Vec = checker .settings .flake8_bugbear .extend_immutable_calls .iter() - .map(|target| CallPath::from_qualified_name(target)) + .map(|target| QualifiedName::from_dotted_name(target)) .collect(); if is_mutable_expr(default, checker.semantic()) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index e23b17b308815..35c929b91341b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -40,8 +40,8 @@ impl Violation for NoExplicitStacklevel { pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall) { if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["warnings", "warn"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["warnings", "warn"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs index 7e13a4c9f126f..06bbc8cdd8237 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/re_sub_positional_args.rs @@ -63,8 +63,8 @@ pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall let Some(method) = checker .semantic() - .resolve_call_path(&call.func) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(&call.func) + .and_then(|qualified_name| match qualified_name.segments() { ["re", "sub"] => Some(Method::Sub), ["re", "subn"] => Some(Method::Subn), ["re", "split"] => Some(Method::Split), diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index da034e01fd4ec..269c4c3715133 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -310,8 +310,8 @@ pub(crate) fn reuse_of_groupby_generator( // Check if the function call is `itertools.groupby` if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["itertools", "groupby"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["itertools", "groupby"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs index 87064fb167bb5..e96b6c631dac8 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/useless_contextlib_suppress.rs @@ -59,8 +59,10 @@ pub(crate) fn useless_contextlib_suppress( if args.is_empty() && checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["contextlib", "suppress"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["contextlib", "suppress"]) + }) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index c8cb82b3f7620..f2514b7d42d70 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -99,32 +99,34 @@ fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { return false; }; - semantic.resolve_call_path(func).is_some_and(|call_path| { - match call_path.segments() { - ["itertools", "cycle" | "count"] => true, - ["itertools", "repeat"] => { - // Ex) `itertools.repeat(1)` - if keywords.is_empty() && args.len() == 1 { - return true; - } + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + match qualified_name.segments() { + ["itertools", "cycle" | "count"] => true, + ["itertools", "repeat"] => { + // Ex) `itertools.repeat(1)` + if keywords.is_empty() && args.len() == 1 { + return true; + } - // Ex) `itertools.repeat(1, None)` - if args.len() == 2 && args[1].is_none_literal_expr() { - return true; - } + // Ex) `itertools.repeat(1, None)` + if args.len() == 2 && args[1].is_none_literal_expr() { + return true; + } - // Ex) `iterools.repeat(1, times=None)` - for keyword in keywords.iter() { - if keyword.arg.as_ref().is_some_and(|name| name == "times") { - if keyword.value.is_none_literal_expr() { - return true; + // Ex) `iterools.repeat(1, times=None)` + for keyword in keywords.iter() { + if keyword.arg.as_ref().is_some_and(|name| name == "times") { + if keyword.value.is_none_literal_expr() { + return true; + } } } - } - false + false + } + _ => false, } - _ => false, - } - }) + }) } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs index 022ee502c14a6..1a022a3010fb0 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_fromtimestamp.rs @@ -64,9 +64,12 @@ pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, locati if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["datetime", "date", "fromtimestamp"]) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["datetime", "date", "fromtimestamp"] + ) }) { checker diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs index 47b4c724b1de6..9e0bacbff8921 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_date_today.rs @@ -63,8 +63,10 @@ pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: Text if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "date", "today"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["datetime", "date", "today"]) + }) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index fda8c13d6719f..30e67d827e06e 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -67,10 +67,10 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::Exp if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["datetime", "datetime", "fromtimestamp"] ) }) diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 793cbb24069ac..6a40aa2734b40 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -63,8 +63,10 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "datetime", "now"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["datetime", "datetime", "now"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 7fbb606b1ba02..9fdce6ee34829 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -71,9 +71,12 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: & if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["datetime", "datetime", "strptime"]) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["datetime", "datetime", "strptime"] + ) }) { return; diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs index 567c6cdcfd675..d712f604ec1d7 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_today.rs @@ -62,8 +62,10 @@ pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "datetime", "today"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["datetime", "datetime", "today"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs index 7c7c0865fff86..2db2649e73793 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcfromtimestamp.rs @@ -70,10 +70,10 @@ pub(crate) fn call_datetime_utcfromtimestamp( if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| { + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["datetime", "datetime", "utcfromtimestamp"] ) }) diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs index e8fc86eae90f3..9ff0de0549cb3 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_utcnow.rs @@ -66,8 +66,13 @@ pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "datetime", "utcnow"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["datetime", "datetime", "utcnow"] + ) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index 253bd3d60fc12..5c3bc9cec39a4 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -59,8 +59,8 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::Ex if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "datetime"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["datetime", "datetime"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs index 52089715a00a8..615e7a7916772 100644 --- a/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs +++ b/crates/ruff_linter/src/rules/flake8_debugger/rules/debugger.rs @@ -2,7 +2,7 @@ use ruff_python_ast::{Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::{CallPath, CallPathBuilder}; +use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -48,16 +48,17 @@ impl Violation for Debugger { /// Checks for the presence of a debugger call. pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { - if let Some(using_type) = checker - .semantic() - .resolve_call_path(func) - .and_then(|call_path| { - if is_debugger_call(&call_path) { - Some(DebuggerUsingType::Call(call_path.to_string())) - } else { - None - } - }) + if let Some(using_type) = + checker + .semantic() + .resolve_qualified_name(func) + .and_then(|qualified_name| { + if is_debugger_call(&qualified_name) { + Some(DebuggerUsingType::Call(qualified_name.to_string())) + } else { + None + } + }) { checker .diagnostics @@ -68,22 +69,23 @@ pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { /// Checks for the presence of a debugger import. pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option { if let Some(module) = module { - let mut builder = CallPathBuilder::from_path(CallPath::from_unqualified_name(module)); + let mut builder = + QualifiedNameBuilder::from_qualified_name(QualifiedName::imported(module)); builder.push(name); - let call_path = builder.build(); + let qualified_name = builder.build(); - if is_debugger_call(&call_path) { + if is_debugger_call(&qualified_name) { return Some(Diagnostic::new( Debugger { - using_type: DebuggerUsingType::Import(call_path.to_string()), + using_type: DebuggerUsingType::Import(qualified_name.to_string()), }, stmt.range(), )); } } else { - let call_path: CallPath = CallPath::from_unqualified_name(name); + let qualified_name = QualifiedName::imported(name); - if is_debugger_import(&call_path) { + if is_debugger_import(&qualified_name) { return Some(Diagnostic::new( Debugger { using_type: DebuggerUsingType::Import(name.to_string()), @@ -95,9 +97,9 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> None } -fn is_debugger_call(call_path: &CallPath) -> bool { +fn is_debugger_call(qualified_name: &QualifiedName) -> bool { matches!( - call_path.segments(), + qualified_name.segments(), ["pdb" | "pudb" | "ipdb", "set_trace"] | ["ipdb", "sset_trace"] | ["IPython", "terminal", "embed", "InteractiveShellEmbed"] @@ -115,13 +117,13 @@ fn is_debugger_call(call_path: &CallPath) -> bool { ) } -fn is_debugger_import(call_path: &CallPath) -> bool { +fn is_debugger_import(qualified_name: &QualifiedName) -> bool { // Constructed by taking every pattern in `is_debugger_call`, removing the last element in // each pattern, and de-duplicating the values. // As a special-case, we omit `builtins` to allow `import builtins`, which is far more general // than (e.g.) `import celery.contrib.rdb`. matches!( - call_path.segments(), + qualified_name.segments(), ["pdb" | "pudb" | "ipdb" | "debugpy" | "ptvsd"] | ["IPython", "terminal", "embed"] | ["IPython", "frontend", "terminal", "embed",] diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs index e1d68c4273658..2a8198021a262 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs @@ -4,16 +4,19 @@ use ruff_python_semantic::{analyze, SemanticModel}; /// Return `true` if a Python class appears to be a Django model, based on its base classes. pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { - analyze::class::any_call_path(class_def, semantic, &|call_path| { - matches!(call_path.segments(), ["django", "db", "models", "Model"]) + analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| { + matches!( + qualified_name.segments(), + ["django", "db", "models", "Model"] + ) }) } /// Return `true` if a Python class appears to be a Django model form, based on its base classes. pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { - analyze::class::any_call_path(class_def, semantic, &|call_path| { + analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"] ) }) @@ -21,11 +24,13 @@ pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticMo /// Return `true` if the expression is constructor for a Django model field. pub(super) fn is_model_field(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(expr).is_some_and(|call_path| { - call_path - .segments() - .starts_with(&["django", "db", "models"]) - }) + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + qualified_name + .segments() + .starts_with(&["django", "db", "models"]) + }) } /// Return the name of the field type, if the expression is constructor for a Django model field. @@ -33,11 +38,13 @@ pub(super) fn get_model_field_name<'a>( expr: &'a Expr, semantic: &'a SemanticModel, ) -> Option<&'a str> { - semantic.resolve_call_path(expr).and_then(|call_path| { - let call_path = call_path.segments(); - if !call_path.starts_with(&["django", "db", "models"]) { - return None; - } - call_path.last().copied() - }) + semantic + .resolve_qualified_name(expr) + .and_then(|qualified_name| { + let qualified_name = qualified_name.segments(); + if !qualified_name.starts_with(&["django", "db", "models"]) { + return None; + } + qualified_name.last().copied() + }) } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs index 3c80332a82e82..9785a2cb9426a 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -51,8 +51,10 @@ pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprC if !checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["django", "shortcuts", "render"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["django", "shortcuts", "render"]) + }) { return; } @@ -72,6 +74,6 @@ fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { return false; }; semantic - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "locals"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "locals"])) } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index eedd93b838807..52118e6b67df8 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -61,9 +61,12 @@ pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_li let is_receiver = decorator.expression.as_call_expr().is_some_and(|call| { checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["django", "dispatch", "receiver"]) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["django", "dispatch", "receiver"] + ) }) }); if i > 0 && is_receiver && !seen_receiver { diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index 9a088494e3475..b719ed7394778 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -80,7 +80,7 @@ impl Violation for FutureRewritableTypeAnnotation { pub(crate) fn future_rewritable_type_annotation(checker: &mut Checker, expr: &Expr) { let name = checker .semantic() - .resolve_call_path(expr) + .resolve_qualified_name(expr) .map(|binding| binding.to_string()); if let Some(name) = name { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs index 46992de2efa58..ee93f507c4ee0 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs @@ -61,8 +61,8 @@ pub(crate) fn direct_logger_instantiation(checker: &mut Checker, call: &ast::Exp if checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["logging", "Logger"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["logging", "Logger"])) { let mut diagnostic = Diagnostic::new(DirectLoggerInstantiation, call.func.range()); diagnostic.try_set_fix(|| { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs index cff88e5ab908d..3953dd215c739 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/exception_without_exc_info.rs @@ -62,8 +62,10 @@ pub(crate) fn exception_without_exc_info(checker: &mut Checker, call: &ExprCall) Expr::Name(_) => { if !checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["logging", "exception"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["logging", "exception"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs index dadffe3473963..1133b9dbcf3f2 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs @@ -77,8 +77,8 @@ pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::Exp if !checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["logging", "getLogger"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["logging", "getLogger"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs index 6a08cf811e1e4..8e38ad5e55a6a 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs @@ -55,8 +55,8 @@ pub(crate) fn undocumented_warn(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["logging", "WARN"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["logging", "WARN"])) { let mut diagnostic = Diagnostic::new(UndocumentedWarn, expr.range()); diagnostic.try_set_fix(|| { diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs index ce3a3d8beeb0b..124759b1f6d59 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs @@ -110,8 +110,8 @@ fn check_log_record_attr_clash(checker: &mut Checker, extra: &Keyword) { }) => { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "dict"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "dict"])) { for keyword in keywords.iter() { if let Some(attr) = &keyword.arg { @@ -165,10 +165,13 @@ pub(crate) fn logging_call(checker: &mut Checker, call: &ast::ExprCall) { (call_type, attr.range()) } Expr::Name(_) => { - let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else { + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + else { return; }; - let ["logging", attribute] = call_path.segments() else { + let ["logging", attribute] = qualified_name.segments() else { return; }; let Some(call_type) = LoggingCallType::from_attribute(attribute) else { diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs index f960db6ffc8ad..3b41b15e75d66 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/non_unique_enums.rs @@ -62,8 +62,8 @@ pub(crate) fn non_unique_enums(checker: &mut Checker, parent: &Stmt, body: &[Stm if !parent.bases().iter().any(|expr| { checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["enum", "Enum"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["enum", "Enum"])) }) { return; } @@ -77,8 +77,8 @@ pub(crate) fn non_unique_enums(checker: &mut Checker, parent: &Stmt, body: &[Stm if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["enum", "auto"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["enum", "auto"])) { continue; } diff --git a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs index 994e1afeb122c..6f7bf3ccb131f 100644 --- a/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs +++ b/crates/ruff_linter/src/rules/flake8_print/rules/print_call.rs @@ -98,30 +98,31 @@ impl Violation for PPrint { /// T201, T203 pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) { let mut diagnostic = { - let call_path = checker.semantic().resolve_call_path(&call.func); - if call_path + let qualified_name = checker.semantic().resolve_qualified_name(&call.func); + if qualified_name .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["", "print"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"])) { // If the print call has a `file=` argument (that isn't `None`, `"sys.stdout"`, // or `"sys.stderr"`), don't trigger T201. if let Some(keyword) = call.arguments.find_keyword("file") { if !keyword.value.is_none_literal_expr() { - if checker.semantic().resolve_call_path(&keyword.value).map_or( - true, - |call_path| { - call_path.segments() != ["sys", "stdout"] - && call_path.segments() != ["sys", "stderr"] - }, - ) { + if checker + .semantic() + .resolve_qualified_name(&keyword.value) + .map_or(true, |qualified_name| { + qualified_name.segments() != ["sys", "stdout"] + && qualified_name.segments() != ["sys", "stderr"] + }) + { return; } } } Diagnostic::new(Print, call.func.range()) - } else if call_path + } else if qualified_name .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["pprint", "pprint"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pprint", "pprint"])) { Diagnostic::new(PPrint, call.func.range()) } else { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs index 45cc221c6d340..4427a643351c6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_generator_return_type.rs @@ -125,10 +125,10 @@ pub(crate) fn bad_generator_return_type( // Determine the module from which the existing annotation is imported (e.g., `typing` or // `collections.abc`) let (method, module, member) = { - let Some(call_path) = semantic.resolve_call_path(map_subscript(returns)) else { + let Some(qualified_name) = semantic.resolve_qualified_name(map_subscript(returns)) else { return; }; - match (name, call_path.segments()) { + match (name, qualified_name.segments()) { ("__iter__", ["typing", "Generator"]) => { (Method::Iter, Module::Typing, Generator::Generator) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 236635df811dc..056e89e3ead37 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -75,8 +75,8 @@ pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) { if !checker .semantic() - .resolve_call_path(left) - .is_some_and(|call_path| matches!(call_path.segments(), ["sys", "version_info"])) + .resolve_qualified_name(left) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "version_info"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs index d1c2ca8f67aba..ea873ebf95221 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -57,8 +57,10 @@ pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["collections", "namedtuple"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["collections", "namedtuple"]) + }) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs index 4b3fe264ef8c5..4c5471e7c6c7f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -67,9 +67,12 @@ pub(crate) fn complex_if_statement_in_stub(checker: &mut Checker, test: &Expr) { if checker .semantic() - .resolve_call_path(left) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["sys", "version_info" | "platform"]) + .resolve_qualified_name(left) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["sys", "version_info" | "platform"] + ) }) { return; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs index d6f8695b3b285..19af0bd6f5e80 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -244,11 +244,11 @@ fn non_none_annotation_element<'a>( ) -> Option<&'a Expr> { // E.g., `typing.Union` or `typing.Optional` if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation { - let call_path = semantic.resolve_call_path(value); + let qualified_name = semantic.resolve_qualified_name(value); - if call_path + if qualified_name .as_ref() - .is_some_and(|value| semantic.match_typing_call_path(value, "Optional")) + .is_some_and(|value| semantic.match_typing_qualified_name(value, "Optional")) { return if slice.is_none_literal_expr() { None @@ -257,9 +257,9 @@ fn non_none_annotation_element<'a>( }; } - if !call_path + if !qualified_name .as_ref() - .is_some_and(|value| semantic.match_typing_call_path(value, "Union")) + .is_some_and(|value| semantic.match_typing_qualified_name(value, "Union")) { return None; } @@ -305,11 +305,11 @@ fn non_none_annotation_element<'a>( /// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type. fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(expr) + .resolve_qualified_name(expr) .as_ref() - .is_some_and(|call_path| { + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["" | "builtins", "object"] | ["_typeshed", "Unused"] ) }) @@ -318,17 +318,24 @@ 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_call_path(expr) + .resolve_qualified_name(expr) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["" | "builtins", "BaseException"])) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["" | "builtins", "BaseException"] + ) + }) } /// Return `true` if the [`Expr`] is the `types.TracebackType` type. fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(expr) + .resolve_qualified_name(expr) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["types", "TracebackType"])) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["types", "TracebackType"]) + }) } /// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`. @@ -339,9 +346,11 @@ fn is_base_exception_type(expr: &Expr, semantic: &SemanticModel) -> bool { if semantic.match_typing_expr(value, "Type") || semantic - .resolve_call_path(value) + .resolve_qualified_name(value) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["" | "builtins", "type"])) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["" | "builtins", "type"]) + }) { is_base_exception(slice, semantic) } else { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index 06007a9e8b773..aec883d862bb6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -88,16 +88,16 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De if checker .semantic() - .resolve_call_path(map_subscript(annotation)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(annotation)) + .is_some_and(|qualified_name| { if is_async { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "AsyncIterable"] | ["collections", "abc", "AsyncIterable"] ) } else { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "Iterable"] | ["collections", "abc", "Iterable"] ) } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 9c08f7b43e4f2..c946eba080107 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -231,12 +231,14 @@ fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool /// Returns `true` if the given expression resolves to a metaclass. fn is_metaclass_base(base: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(base).is_some_and(|call_path| { - matches!( - call_path.segments(), - ["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"] - ) - }) + semantic + .resolve_qualified_name(base) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"] + ) + }) } /// Returns `true` if the method is an in-place binary operator. @@ -279,10 +281,10 @@ fn is_iterator(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool }; bases.iter().any(|expr| { semantic - .resolve_call_path(map_subscript(expr)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(expr)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "Iterator"] | ["collections", "abc", "Iterator"] ) }) @@ -292,10 +294,10 @@ fn is_iterator(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool /// Return `true` if the given expression resolves to `collections.abc.Iterable`. fn is_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(map_subscript(expr)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(expr)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "Iterable" | "Iterator"] | ["collections", "abc", "Iterable" | "Iterator"] ) @@ -309,10 +311,10 @@ fn is_async_iterator(arguments: Option<&Arguments>, semantic: &SemanticModel) -> }; bases.iter().any(|expr| { semantic - .resolve_call_path(map_subscript(expr)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(expr)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"] ) }) @@ -322,10 +324,10 @@ fn is_async_iterator(arguments: Option<&Arguments>, semantic: &SemanticModel) -> /// Return `true` if the given expression resolves to `collections.abc.AsyncIterable`. fn is_async_iterable(expr: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(map_subscript(expr)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(expr)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["typing", "AsyncIterable" | "AsyncIterator"] | ["collections", "abc", "AsyncIterable" | "AsyncIterator"] ) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs index 670a30ce39c6a..e4c237c3cb807 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -81,21 +81,21 @@ pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: & let Some(kind) = checker .semantic() - .resolve_call_path(func) - .and_then(|call_path| { + .resolve_qualified_name(func) + .and_then(|qualified_name| { if checker .semantic() - .match_typing_call_path(&call_path, "ParamSpec") + .match_typing_qualified_name(&qualified_name, "ParamSpec") { Some(VarKind::ParamSpec) } else if checker .semantic() - .match_typing_call_path(&call_path, "TypeVar") + .match_typing_qualified_name(&qualified_name, "TypeVar") { Some(VarKind::TypeVar) } else if checker .semantic() - .match_typing_call_path(&call_path, "TypeVarTuple") + .match_typing_qualified_name(&qualified_name, "TypeVarTuple") { Some(VarKind::TypeVarTuple) } else { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index bb1744388b2ab..672e4c40f4993 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -90,11 +90,11 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) { let mut has_int = false; let mut func = |expr: &Expr, _parent: &Expr| { - let Some(call_path) = checker.semantic().resolve_call_path(expr) else { + let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) else { return; }; - match call_path.segments() { + match qualified_name.segments() { ["" | "builtins", "int"] => has_int = true, ["" | "builtins", "float"] => has_float = true, ["" | "builtins", "complex"] => has_complex = true, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 6f37244a558ea..fdf868aba09ac 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -2,8 +2,8 @@ use rustc_hash::FxHashSet; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::map_subscript; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{ self as ast, Expr, Operator, ParameterWithDefault, Parameters, Stmt, UnaryOp, }; @@ -248,13 +248,16 @@ impl AlwaysFixableViolation for TypeAliasWithoutAnnotation { } } -fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool { - matches!(call_path.segments(), ["math", "inf" | "e" | "pi" | "tau"]) +fn is_allowed_negated_math_attribute(qualified_name: &QualifiedName) -> bool { + matches!( + qualified_name.segments(), + ["math", "inf" | "e" | "pi" | "tau"] + ) } -fn is_allowed_math_attribute(call_path: &CallPath) -> bool { +fn is_allowed_math_attribute(qualified_name: &QualifiedName) -> bool { matches!( - call_path.segments(), + qualified_name.segments(), ["math", "inf" | "nan" | "e" | "pi" | "tau"] | [ "sys", @@ -324,7 +327,7 @@ fn is_valid_default_value_with_annotation( // Ex) `-math.inf`, `-math.pi`, etc. Expr::Attribute(_) => { if semantic - .resolve_call_path(operand) + .resolve_qualified_name(operand) .as_ref() .is_some_and(is_allowed_negated_math_attribute) { @@ -373,7 +376,7 @@ fn is_valid_default_value_with_annotation( // Ex) `math.inf`, `sys.stdin`, etc. Expr::Attribute(_) => { if semantic - .resolve_call_path(default) + .resolve_qualified_name(default) .as_ref() .is_some_and(is_allowed_math_attribute) { @@ -435,15 +438,17 @@ fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; }; - semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!( - call_path.segments(), - [ - "typing" | "typing_extensions", - "TypeVar" | "TypeVarTuple" | "NewType" | "ParamSpec" - ] - ) - }) + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + [ + "typing" | "typing_extensions", + "TypeVar" | "TypeVarTuple" | "NewType" | "ParamSpec" + ] + ) + }) } /// Returns `true` if this is a "special" assignment which must have a value (e.g., an assignment to @@ -480,10 +485,10 @@ fn is_enum(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { class_def.bases().iter().any(|expr| { // If the base class is `enum.Enum`, `enum.Flag`, etc., then this is an enum. if semantic - .resolve_call_path(map_subscript(expr)) - .is_some_and(|call_path| { + .resolve_qualified_name(map_subscript(expr)) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "enum", "Enum" | "Flag" | "IntEnum" | "IntFlag" | "StrEnum" | "ReprEnum" diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 197032e8edefe..d8a3f3d3e9dd0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -80,9 +80,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { if checker .semantic() - .resolve_call_path(returns) - .map_or(true, |call_path| { - !matches!(call_path.segments(), ["" | "builtins", "str"]) + .resolve_qualified_name(returns) + .map_or(true, |qualified_name| { + !matches!(qualified_name.segments(), ["" | "builtins", "str"]) }) { return; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 63572c96433bf..7be2510933e32 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -84,10 +84,10 @@ fn is_warnings_dot_deprecated(expr: Option<&ast::Expr>, semantic: &SemanticModel return false; }; semantic - .resolve_call_path(&call.func) - .is_some_and(|call_path| { + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["warnings" | "typing_extensions", "deprecated"] ) }) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs index d2bcc234088cb..0a29b9ad28694 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_type_union.rs @@ -80,8 +80,10 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr) let unwrapped = subscript.unwrap(); if checker .semantic() - .resolve_call_path(unwrapped.value.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["" | "builtins", "type"])) + .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 { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs index 160dbd5e70db2..3565e5d6a8fb2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -107,8 +107,8 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) { if !checker .semantic() - .resolve_call_path(left) - .is_some_and(|call_path| matches!(call_path.segments(), ["sys", "platform"])) + .resolve_qualified_name(left) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "platform"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs index b8ff0c87ab73a..7bc2180afe5f9 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -135,8 +135,8 @@ pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) { if !checker .semantic() - .resolve_call_path(map_subscript(left)) - .is_some_and(|call_path| matches!(call_path.segments(), ["sys", "version_info"])) + .resolve_qualified_name(map_subscript(left)) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "version_info"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs index 16c2bf1ec5f0c..cf4f7248b8aba 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs @@ -195,17 +195,22 @@ pub(crate) fn unused_private_type_var( }; let semantic = checker.semantic(); - let Some(type_var_like_kind) = semantic.resolve_call_path(func).and_then(|call_path| { - if semantic.match_typing_call_path(&call_path, "TypeVar") { - Some("TypeVar") - } else if semantic.match_typing_call_path(&call_path, "ParamSpec") { - Some("ParamSpec") - } else if semantic.match_typing_call_path(&call_path, "TypeVarTuple") { - Some("TypeVarTuple") - } else { - None - } - }) else { + let Some(type_var_like_kind) = + semantic + .resolve_qualified_name(func) + .and_then(|qualified_name| { + if semantic.match_typing_qualified_name(&qualified_name, "TypeVar") { + Some("TypeVar") + } else if semantic.match_typing_qualified_name(&qualified_name, "ParamSpec") { + Some("ParamSpec") + } else if semantic.match_typing_qualified_name(&qualified_name, "TypeVarTuple") + { + Some("TypeVarTuple") + } else { + None + } + }) + else { continue; }; diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index dafd200d9c4d8..a65f59915f16e 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -3,8 +3,8 @@ use std::fmt; use ruff_diagnostics::{AlwaysFixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; use ruff_python_ast::identifier::Identifier; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::Decorator; @@ -646,9 +646,9 @@ impl<'a> Visitor<'a> for SkipFunctionsVisitor<'a> { } } Expr::Call(ast::ExprCall { func, .. }) => { - if CallPath::from_expr(func).is_some_and(|call_path| { - matches!(call_path.segments(), ["request", "addfinalizer"]) - }) { + if UnqualifiedName::from_expr(func) + .is_some_and(|name| matches!(name.segments(), ["request", "addfinalizer"])) + { self.addfinalizer_call = Some(expr); }; visitor::walk_expr(self, expr); diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs index 59f85c0a359d0..664dd8878eeeb 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs @@ -1,7 +1,6 @@ -use ruff_python_ast::call_path::CallPath; -use ruff_python_ast::{self as ast, Decorator, Expr, Keyword}; - use ruff_python_ast::helpers::map_callable; +use ruff_python_ast::name::UnqualifiedName; +use ruff_python_ast::{self as ast, Decorator, Expr, Keyword}; use ruff_python_semantic::SemanticModel; use ruff_python_trivia::PythonWhitespace; @@ -9,10 +8,10 @@ pub(super) fn get_mark_decorators( decorators: &[Decorator], ) -> impl Iterator { decorators.iter().filter_map(|decorator| { - let Some(call_path) = CallPath::from_expr(map_callable(&decorator.expression)) else { + let Some(name) = UnqualifiedName::from_expr(map_callable(&decorator.expression)) else { return None; }; - let ["pytest", "mark", marker] = call_path.segments() else { + let ["pytest", "mark", marker] = name.segments() else { return None; }; Some((decorator, *marker)) @@ -21,26 +20,30 @@ pub(super) fn get_mark_decorators( pub(super) fn is_pytest_fail(call: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(call) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "fail"])) + .resolve_qualified_name(call) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fail"])) } pub(super) fn is_pytest_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(map_callable(&decorator.expression)) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "fixture"])) + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "fixture"])) } pub(super) fn is_pytest_yield_fixture(decorator: &Decorator, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(map_callable(&decorator.expression)) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "yield_fixture"])) + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pytest", "yield_fixture"]) + }) } pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(map_callable(&decorator.expression)) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "mark", "parametrize"])) + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pytest", "mark", "parametrize"]) + }) } pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs index 2a926ffca90d8..1b99eb9e21bbd 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/patch.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, Parameters}; @@ -104,10 +104,10 @@ fn check_patch_call(call: &ast::ExprCall, index: usize) -> Option { /// PT008 pub(crate) fn patch_with_lambda(call: &ast::ExprCall) -> Option { - let call_path = CallPath::from_expr(&call.func)?; + let name = UnqualifiedName::from_expr(&call.func)?; if matches!( - call_path.segments(), + name.segments(), [ "mocker" | "class_mocker" @@ -120,7 +120,7 @@ pub(crate) fn patch_with_lambda(call: &ast::ExprCall) -> Option { ) { check_patch_call(call, 1) } else if matches!( - call_path.segments(), + name.segments(), [ "mocker" | "class_mocker" diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs index a1344263e0758..ea40496c30b0f 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs @@ -153,8 +153,8 @@ impl Violation for PytestRaisesWithoutException { fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pytest", "raises"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"])) } const fn is_non_trivial_with_body(body: &[Stmt]) -> bool { @@ -226,11 +226,11 @@ pub(crate) fn complex_raises( /// PT011 fn exception_needs_match(checker: &mut Checker, exception: &Expr) { - if let Some(call_path) = checker + if let Some(qualified_name) = checker .semantic() - .resolve_call_path(exception) - .and_then(|call_path| { - let call_path = call_path.to_string(); + .resolve_qualified_name(exception) + .and_then(|qualified_name| { + let qualified_name = qualified_name.to_string(); checker .settings .flake8_pytest_style @@ -242,13 +242,13 @@ fn exception_needs_match(checker: &mut Checker, exception: &Expr) { .flake8_pytest_style .raises_extend_require_match_for, ) - .any(|pattern| pattern.matches(&call_path)) - .then_some(call_path) + .any(|pattern| pattern.matches(&qualified_name)) + .then_some(qualified_name) }) { checker.diagnostics.push(Diagnostic::new( PytestRaisesTooBroad { - exception: call_path, + exception: qualified_name, }, exception.range(), )); diff --git a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index 8ed01447b4cf0..0c9e2477fce26 100644 --- a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -99,8 +99,10 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr: // we might as well get it right. if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["ctypes", "WinError"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["ctypes", "WinError"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs index bf211d3d189c4..615f83b6b18c0 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -400,16 +400,19 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) { fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { // First, look for known functions that never return from the standard library and popular // libraries. - if semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!( - call_path.segments(), - ["" | "builtins" | "sys" | "_thread" | "pytest", "exit"] - | ["" | "builtins", "quit"] - | ["os" | "posix", "_exit" | "abort"] - | ["_winapi", "ExitProcess"] - | ["pytest", "fail" | "skip" | "xfail"] - ) || semantic.match_typing_call_path(&call_path, "assert_never") - }) { + if semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["" | "builtins" | "sys" | "_thread" | "pytest", "exit"] + | ["" | "builtins", "quit"] + | ["os" | "posix", "_exit" | "abort"] + | ["_winapi", "ExitProcess"] + | ["pytest", "fail" | "skip" | "xfail"] + ) || semantic.match_typing_qualified_name(&qualified_name, "assert_never") + }) + { return true; } @@ -430,11 +433,11 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { return false; }; - let Some(call_path) = semantic.resolve_call_path(returns) else { + let Some(qualified_name) = semantic.resolve_qualified_name(returns) else { return false; }; - semantic.match_typing_call_path(&call_path, "NoReturn") + semantic.match_typing_qualified_name(&qualified_name, "NoReturn") } /// RET503 diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index 25a5d0771726b..f583c22d6cab0 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -198,8 +198,8 @@ fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool else { return false; }; - if let Some(call_path) = semantic.resolve_call_path(func) { - if call_path.segments() == ["contextlib", "suppress"] { + if let Some(qualified_name) = semantic.resolve_qualified_name(func) { + if qualified_name.segments() == ["contextlib", "suppress"] { return true; } } diff --git a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs index 37f6898a6c20a..f81024f123f9a 100644 --- a/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs +++ b/crates/ruff_linter/src/rules/flake8_self/rules/private_member_access.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::{BindingKind, ScopeKind}; use ruff_text_size::Ranged; @@ -141,24 +141,24 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) { } // Allow some documented private methods, like `os._exit()`. - if let Some(call_path) = checker.semantic().resolve_call_path(expr) { - if matches!(call_path.segments(), ["os", "_exit"]) { + if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) { + if matches!(qualified_name.segments(), ["os", "_exit"]) { return; } } if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() { // Ignore `super()` calls. - if let Some(call_path) = CallPath::from_expr(func) { - if matches!(call_path.segments(), ["super"]) { + if let Some(name) = UnqualifiedName::from_expr(func) { + if matches!(name.segments(), ["super"]) { return; } } } - if let Some(call_path) = CallPath::from_expr(value) { + if let Some(name) = UnqualifiedName::from_expr(value) { // Ignore `self` and `cls` accesses. - if matches!(call_path.segments(), ["self" | "cls" | "mcs"]) { + if matches!(name.segments(), ["self" | "cls" | "mcs"]) { return; } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index 07190acb678bc..dd6cfa545d0a0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -149,10 +149,10 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex }; if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| { + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["os", "environ", "get"] | ["os", "getenv"] ) }) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs index fd720f03abcef..881f4b36914a4 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs @@ -99,10 +99,10 @@ fn explicit_with_items(checker: &mut Checker, with_items: &[WithItem]) -> bool { }; checker .semantic() - .resolve_call_path(&expr_call.func) - .is_some_and(|call_path| { + .resolve_qualified_name(&expr_call.func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["asyncio", "timeout" | "timeout_at"] | ["anyio", "CancelScope" | "fail_after" | "move_on_after"] | [ diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index a6ccb34ec3269..148ed9187bad0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -63,9 +63,12 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!(call_path.segments(), ["contextlib", "AsyncExitStack"]) - }) { + if semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["contextlib", "AsyncExitStack"]) + }) + { return true; } } @@ -94,9 +97,12 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool { if let Stmt::With(ast::StmtWith { items, .. }) = parent { for item in items { if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr { - if semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!(call_path.segments(), ["contextlib", "ExitStack"]) - }) { + if semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["contextlib", "ExitStack"]) + }) + { return true; } } @@ -114,8 +120,10 @@ fn is_open(checker: &mut Checker, func: &Expr) -> bool { match value.as_ref() { Expr::Call(ast::ExprCall { func, .. }) => checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pathlib", "Path"])), + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pathlib", "Path"]) + }), _ => false, } } diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs index 4d841b3a05f62..bdc73c62eb119 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/suppressible_exception.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; use ruff_python_ast::helpers; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, ExceptHandler, Stmt}; use ruff_text_size::Ranged; use ruff_text_size::{TextLen, TextRange}; @@ -107,7 +107,7 @@ pub(crate) fn suppressible_exception( let Some(handler_names) = helpers::extract_handled_exceptions(handlers) .into_iter() - .map(compose_call_path) + .map(|expr| UnqualifiedName::from_expr(expr).map(|name| name.to_string())) .collect::>>() else { return; diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index 1c3cd379f2841..187e0bcdf9062 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -73,8 +73,10 @@ pub(crate) fn no_slots_in_namedtuple_subclass( }; checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["collections", "namedtuple"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["collections", "namedtuple"]) + }) }) { if !has_slots(&class.body) { checker.diagnostics.push(Diagnostic::new( diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs index 9cae4c6b6bafa..4c9006ff03f79 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -69,8 +69,8 @@ pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class fn is_str_subclass(bases: &[Expr], semantic: &SemanticModel) -> bool { let mut is_str_subclass = false; for base in bases { - if let Some(call_path) = semantic.resolve_call_path(base) { - match call_path.segments() { + if let Some(qualified_name) = semantic.resolve_qualified_name(base) { + match qualified_name.segments() { ["" | "builtins", "str"] => { is_str_subclass = true; } diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs index 78f61bf3e191c..0c0c1462685ae 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -58,12 +58,12 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla if bases.iter().any(|base| { checker .semantic() - .resolve_call_path(map_subscript(base)) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["" | "builtins", "tuple"]) + .resolve_qualified_name(map_subscript(base)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["" | "builtins", "tuple"]) || checker .semantic() - .match_typing_call_path(&call_path, "Tuple") + .match_typing_qualified_name(&qualified_name, "Tuple") }) }) { if !has_slots(&class.body) { diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs index 9efeb95c1a8ea..534d408e16e10 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/rules/banned_api.rs @@ -2,7 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -65,10 +65,10 @@ pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) { if let Some((banned_path, ban)) = checker .semantic() - .resolve_call_path(expr) - .and_then(|call_path| { + .resolve_qualified_name(expr) + .and_then(|qualified_name| { banned_api.iter().find(|(banned_path, ..)| { - call_path == CallPath::from_qualified_name(banned_path) + qualified_name == QualifiedName::from_dotted_name(banned_path) }) }) { diff --git a/crates/ruff_linter/src/rules/flake8_trio/method_name.rs b/crates/ruff_linter/src/rules/flake8_trio/method_name.rs index 14a9806c6c315..1eabbc0548bd0 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/method_name.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/method_name.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(super) enum MethodName { @@ -70,8 +70,8 @@ impl MethodName { } impl MethodName { - pub(super) fn try_from(call_path: &CallPath<'_>) -> Option { - match call_path.segments() { + pub(super) fn try_from(qualified_name: &QualifiedName<'_>) -> Option { + match qualified_name.segments() { ["trio", "CancelScope"] => Some(Self::CancelScope), ["trio", "aclose_forcefully"] => Some(Self::AcloseForcefully), ["trio", "fail_after"] => Some(Self::FailAfter), diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs index 58a4cdf164eb8..2f824ab41a26c 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs @@ -57,10 +57,13 @@ pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) { } let Some(method_name) = ({ - let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else { + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + else { return; }; - MethodName::try_from(&call_path) + MethodName::try_from(&qualified_name) }) else { return; }; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs index 904ccc7205aa1..d0707d32bc4a2 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs @@ -56,8 +56,10 @@ pub(crate) fn timeout_without_await( let Some(method_name) = with_items.iter().find_map(|item| { let call = item.context_expr.as_call_expr()?; - let call_path = checker.semantic().resolve_call_path(call.func.as_ref())?; - MethodName::try_from(&call_path) + let qualified_name = checker + .semantic() + .resolve_qualified_name(call.func.as_ref())?; + MethodName::try_from(&qualified_name) }) else { return; }; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs index 95c41db491a28..921e0adaa9ea2 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs @@ -63,7 +63,7 @@ pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) if checker .semantic() - .resolve_call_path(func.as_ref()) + .resolve_qualified_name(func.as_ref()) .is_some_and(|path| matches!(path.segments(), ["trio", "sleep" | "sleep_until"])) { checker diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs index fff7c999c5f37..515db518180ff 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs @@ -62,8 +62,8 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) { if !checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["trio", "sleep"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["trio", "sleep"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index e5c329484ec65..56c475c9159d9 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -1,8 +1,8 @@ use anyhow::Result; use ruff_diagnostics::Edit; -use ruff_python_ast::call_path::CallPath; use ruff_python_ast::helpers::{map_callable, map_subscript}; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Decorator, Expr}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_semantic::{ @@ -77,10 +77,10 @@ fn runtime_required_base_class( base_classes: &[String], semantic: &SemanticModel, ) -> bool { - analyze::class::any_call_path(class_def, semantic, &|call_path| { + analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| { base_classes .iter() - .any(|base_class| CallPath::from_qualified_name(base_class) == call_path) + .any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name) }) } @@ -95,11 +95,11 @@ fn runtime_required_decorators( 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| { decorators .iter() - .any(|base_class| CallPath::from_qualified_name(base_class) == call_path) + .any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name) }) }) } @@ -119,17 +119,17 @@ pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &Semanti if let ScopeKind::Class(class_def) = semantic.current_scope().kind { if class_def.decorator_list.iter().any(|decorator| { semantic - .resolve_call_path(map_callable(&decorator.expression)) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["dataclasses", "dataclass"]) + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["dataclasses", "dataclass"]) }) }) { // Determine whether the annotation is `typing.ClassVar` or `dataclasses.InitVar`. return semantic - .resolve_call_path(map_subscript(annotation)) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["dataclasses", "InitVar"]) - || semantic.match_typing_call_path(&call_path, "ClassVar") + .resolve_qualified_name(map_subscript(annotation)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["dataclasses", "InitVar"]) + || semantic.match_typing_qualified_name(&qualified_name, "ClassVar") }); } } @@ -154,9 +154,9 @@ pub(crate) fn is_singledispatch_interface( ) -> bool { function_def.decorator_list.iter().any(|decorator| { semantic - .resolve_call_path(&decorator.expression) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["functools", "singledispatch"]) + .resolve_qualified_name(&decorator.expression) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["functools", "singledispatch"]) }) }) } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs index 66c7f13352144..c6dab7b57ea0d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_sep_split.rs @@ -78,8 +78,8 @@ pub(crate) fn os_sep_split(checker: &mut Checker, call: &ast::ExprCall) { if !checker .semantic() - .resolve_call_path(sep) - .is_some_and(|call_path| matches!(call_path.segments(), ["os", "sep"])) + .resolve_qualified_name(sep) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "sep"])) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index f94699e5b2807..542c32dcc55de 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -46,8 +46,10 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory { pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) { if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pathlib", "Path" | "PurePath"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pathlib", "Path" | "PurePath"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 68c01c1d4d168..fabcdd40e3ad7 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -16,142 +16,141 @@ use crate::rules::flake8_use_pathlib::violations::{ use crate::settings::types::PythonVersion; pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, call: &ExprCall) { - if let Some(diagnostic_kind) = - checker - .semantic() - .resolve_call_path(&call.func) - .and_then(|call_path| match call_path.segments() { - // PTH100 - ["os", "path", "abspath"] => Some(OsPathAbspath.into()), - // PTH101 - ["os", "chmod"] => Some(OsChmod.into()), - // PTH102 - ["os", "makedirs"] => Some(OsMakedirs.into()), - // PTH103 - ["os", "mkdir"] => Some(OsMkdir.into()), - // PTH104 - ["os", "rename"] => Some(OsRename.into()), - // PTH105 - ["os", "replace"] => Some(OsReplace.into()), - // PTH106 - ["os", "rmdir"] => Some(OsRmdir.into()), - // PTH107 - ["os", "remove"] => Some(OsRemove.into()), - // PTH108 - ["os", "unlink"] => Some(OsUnlink.into()), - // PTH109 - ["os", "getcwd"] => Some(OsGetcwd.into()), - ["os", "getcwdb"] => Some(OsGetcwd.into()), - // PTH110 - ["os", "path", "exists"] => Some(OsPathExists.into()), - // PTH111 - ["os", "path", "expanduser"] => Some(OsPathExpanduser.into()), - // PTH112 - ["os", "path", "isdir"] => Some(OsPathIsdir.into()), - // PTH113 - ["os", "path", "isfile"] => Some(OsPathIsfile.into()), - // PTH114 - ["os", "path", "islink"] => Some(OsPathIslink.into()), - // PTH116 - ["os", "stat"] => Some(OsStat.into()), - // PTH117 - ["os", "path", "isabs"] => Some(OsPathIsabs.into()), - // PTH118 - ["os", "path", "join"] => Some( - OsPathJoin { - module: "path".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - ["os", "sep", "join"] => Some( - OsPathJoin { - module: "sep".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash - }, - } - .into(), - ), - // PTH119 - ["os", "path", "basename"] => Some(OsPathBasename.into()), - // PTH120 - ["os", "path", "dirname"] => Some(OsPathDirname.into()), - // PTH121 - ["os", "path", "samefile"] => Some(OsPathSamefile.into()), - // PTH122 - ["os", "path", "splitext"] => Some(OsPathSplitext.into()), - // PTH202 - ["os", "path", "getsize"] => Some(OsPathGetsize.into()), - // PTH203 - ["os", "path", "getatime"] => Some(OsPathGetatime.into()), - // PTH204 - ["os", "path", "getmtime"] => Some(OsPathGetmtime.into()), - // PTH205 - ["os", "path", "getctime"] => Some(OsPathGetctime.into()), - // PTH123 - ["" | "builtin", "open"] => { - // `closefd` and `openener` are not supported by pathlib, so check if they are - // are set to non-default values. - // https://github.com/astral-sh/ruff/issues/7620 - // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): - // ```text - // 0 1 2 3 4 5 - // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, - // 6 7 - // closefd=True, opener=None) - // ^^^^ ^^^^ - // ``` - // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): - // ```text - // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) - // ``` - if call + if let Some(diagnostic_kind) = checker + .semantic() + .resolve_qualified_name(&call.func) + .and_then(|qualified_name| match qualified_name.segments() { + // PTH100 + ["os", "path", "abspath"] => Some(OsPathAbspath.into()), + // PTH101 + ["os", "chmod"] => Some(OsChmod.into()), + // PTH102 + ["os", "makedirs"] => Some(OsMakedirs.into()), + // PTH103 + ["os", "mkdir"] => Some(OsMkdir.into()), + // PTH104 + ["os", "rename"] => Some(OsRename.into()), + // PTH105 + ["os", "replace"] => Some(OsReplace.into()), + // PTH106 + ["os", "rmdir"] => Some(OsRmdir.into()), + // PTH107 + ["os", "remove"] => Some(OsRemove.into()), + // PTH108 + ["os", "unlink"] => Some(OsUnlink.into()), + // PTH109 + ["os", "getcwd"] => Some(OsGetcwd.into()), + ["os", "getcwdb"] => Some(OsGetcwd.into()), + // PTH110 + ["os", "path", "exists"] => Some(OsPathExists.into()), + // PTH111 + ["os", "path", "expanduser"] => Some(OsPathExpanduser.into()), + // PTH112 + ["os", "path", "isdir"] => Some(OsPathIsdir.into()), + // PTH113 + ["os", "path", "isfile"] => Some(OsPathIsfile.into()), + // PTH114 + ["os", "path", "islink"] => Some(OsPathIslink.into()), + // PTH116 + ["os", "stat"] => Some(OsStat.into()), + // PTH117 + ["os", "path", "isabs"] => Some(OsPathIsabs.into()), + // PTH118 + ["os", "path", "join"] => Some( + OsPathJoin { + module: "path".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + } + .into(), + ), + ["os", "sep", "join"] => Some( + OsPathJoin { + module: "sep".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, + } + .into(), + ), + // PTH119 + ["os", "path", "basename"] => Some(OsPathBasename.into()), + // PTH120 + ["os", "path", "dirname"] => Some(OsPathDirname.into()), + // PTH121 + ["os", "path", "samefile"] => Some(OsPathSamefile.into()), + // PTH122 + ["os", "path", "splitext"] => Some(OsPathSplitext.into()), + // PTH202 + ["os", "path", "getsize"] => Some(OsPathGetsize.into()), + // PTH203 + ["os", "path", "getatime"] => Some(OsPathGetatime.into()), + // PTH204 + ["os", "path", "getmtime"] => Some(OsPathGetmtime.into()), + // PTH205 + ["os", "path", "getctime"] => Some(OsPathGetctime.into()), + // PTH123 + ["" | "builtin", "open"] => { + // `closefd` and `openener` are not supported by pathlib, so check if they are + // are set to non-default values. + // https://github.com/astral-sh/ruff/issues/7620 + // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): + // ```text + // 0 1 2 3 4 5 + // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, + // 6 7 + // closefd=True, opener=None) + // ^^^^ ^^^^ + // ``` + // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): + // ```text + // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) + // ``` + if call + .arguments + .find_argument("closefd", 6) + .is_some_and(|expr| { + !matches!( + expr, + Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) + ) + }) + || call .arguments - .find_argument("closefd", 6) - .is_some_and(|expr| { - !matches!( - expr, - Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) - ) - }) - || call - .arguments - .find_argument("opener", 7) - .is_some_and(|expr| !expr.is_none_literal_expr()) - { - return None; - } - Some(BuiltinOpen.into()) + .find_argument("opener", 7) + .is_some_and(|expr| !expr.is_none_literal_expr()) + { + return None; + } + Some(BuiltinOpen.into()) + } + // PTH124 + ["py", "path", "local"] => Some(PyPath.into()), + // PTH207 + ["glob", "glob"] => Some( + Glob { + function: "glob".to_string(), } - // PTH124 - ["py", "path", "local"] => Some(PyPath.into()), - // PTH207 - ["glob", "glob"] => Some( - Glob { - function: "glob".to_string(), - } - .into(), - ), - ["glob", "iglob"] => Some( - Glob { - function: "iglob".to_string(), - } - .into(), - ), - // PTH115 - // Python 3.9+ - ["os", "readlink"] if checker.settings.target_version >= PythonVersion::Py39 => { - Some(OsReadlink.into()) + .into(), + ), + ["glob", "iglob"] => Some( + Glob { + function: "iglob".to_string(), } - _ => None, - }) + .into(), + ), + // PTH115 + // Python 3.9+ + ["os", "readlink"] if checker.settings.target_version >= PythonVersion::Py39 => { + Some(OsReadlink.into()) + } + _ => None, + }) { let diagnostic = Diagnostic::new::(diagnostic_kind, call.func.range()); diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs index da3e0de4d61b0..8a3d943a797ff 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs @@ -63,8 +63,8 @@ pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) { if let Some((existing, replacement)) = checker .semantic() - .resolve_call_path(expr) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(expr) + .and_then(|qualified_name| match qualified_name.segments() { ["numpy", "round_"] => Some(("round_", "round")), ["numpy", "product"] => Some(("product", "prod")), ["numpy", "cumproduct"] => Some(("cumproduct", "cumprod")), diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs index b8cfe827b60e7..6f91e68eb128b 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs @@ -54,22 +54,30 @@ pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { return; } - if let Some(type_name) = checker - .semantic() - .resolve_call_path(expr) - .and_then(|call_path| { - if matches!( - call_path.segments(), - [ - "numpy", - "bool" | "int" | "float" | "complex" | "object" | "str" | "long" | "unicode" - ] - ) { - Some(call_path.segments()[1]) - } else { - None - } - }) + if let Some(type_name) = + checker + .semantic() + .resolve_qualified_name(expr) + .and_then(|qualified_name| { + if matches!( + qualified_name.segments(), + [ + "numpy", + "bool" + | "int" + | "float" + | "complex" + | "object" + | "str" + | "long" + | "unicode" + ] + ) { + Some(qualified_name.segments()[1]) + } else { + None + } + }) { let mut diagnostic = Diagnostic::new( NumpyDeprecatedTypeAlias { diff --git a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs index b51bc84e1ac6b..13050db4abfc7 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -64,18 +64,19 @@ pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { return; } - if let Some(method_name) = checker - .semantic() - .resolve_call_path(expr) - .and_then(|call_path| { - // seeding state - if matches!( - call_path.segments(), - [ - "numpy", - "random", - // Seeds - "seed" | + if let Some(method_name) = + checker + .semantic() + .resolve_qualified_name(expr) + .and_then(|qualified_name| { + // seeding state + if matches!( + qualified_name.segments(), + [ + "numpy", + "random", + // Seeds + "seed" | "get_state" | "set_state" | // Simple random data @@ -128,13 +129,13 @@ pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { "wald" | "weibull" | "zipf" - ] - ) { - Some(call_path.segments()[2]) - } else { - None - } - }) + ] + ) { + Some(qualified_name.segments()[2]) + } else { + None + } + }) { checker.diagnostics.push(Diagnostic::new( NumpyLegacyRandom { diff --git a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs index 0d55e71765fc1..b969ca5e6db07 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs @@ -159,8 +159,8 @@ pub(crate) fn numpy_2_0_deprecation(checker: &mut Checker, expr: &Expr) { let maybe_replacement = checker .semantic() - .resolve_call_path(expr) - .and_then(|call_path| match call_path.segments() { + .resolve_qualified_name(expr) + .and_then(|qualified_name| match qualified_name.segments() { // NumPy's main namespace np.* members removed in 2.0 ["numpy", "add_docstring"] => Some(Replacement { existing: "add_docstring", diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs index 4d16db06a118a..e1766b27c4704 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/inplace_argument.rs @@ -55,8 +55,8 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) { // If the function was imported from another module, and it's _not_ Pandas, abort. if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| !matches!(call_path.segments(), ["pandas", ..])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| !matches!(qualified_name.segments(), ["pandas", ..])) { return; } diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs index e819582e4b05c..e9ffb89aa962e 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs @@ -53,8 +53,8 @@ pub(crate) fn use_of_read_table(checker: &mut Checker, call: &ast::ExprCall) { if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pandas", "read_table"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pandas", "read_table"])) { if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = call .arguments diff --git a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs index b2372ed6b1774..068c4b143b95e 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs @@ -1,7 +1,7 @@ use itertools::Itertools; -use ruff_python_ast::call_path::CallPath; -use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; +use ruff_python_ast::name::UnqualifiedName; +use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::str::{is_cased_lowercase, is_cased_uppercase}; @@ -31,10 +31,12 @@ pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) - let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; - semantic.resolve_call_path(func).is_some_and(|call_path| { - matches!(call_path.segments(), ["collections", "namedtuple"]) - || semantic.match_typing_call_path(&call_path, "NamedTuple") - }) + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["collections", "namedtuple"]) + || semantic.match_typing_qualified_name(&qualified_name, "NamedTuple") + }) } /// Returns `true` if the statement is an assignment to a `TypedDict`. @@ -64,10 +66,12 @@ pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> b let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; - semantic.resolve_call_path(func).is_some_and(|call_path| { - semantic.match_typing_call_path(&call_path, "TypeVar") - || semantic.match_typing_call_path(&call_path, "NewType") - }) + semantic + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + semantic.match_typing_qualified_name(&qualified_name, "TypeVar") + || semantic.match_typing_qualified_name(&qualified_name, "NewType") + }) } /// Returns `true` if the statement is an assignment to a `TypeAlias`. @@ -118,8 +122,8 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti } // Match against, e.g., `apps.get_model("zerver", "Attachment")`. - if let Some(call_path) = CallPath::from_expr(func.as_ref()) { - if matches!(call_path.segments(), [.., "get_model"]) { + if let Some(unqualified_name) = UnqualifiedName::from_expr(func.as_ref()) { + if matches!(unqualified_name.segments(), [.., "get_model"]) { if let Some(argument) = arguments.find_argument("model_name", arguments.args.len().saturating_sub(1)) { @@ -135,9 +139,9 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti } // Match against, e.g., `import_string("zerver.models.Attachment")`. - if let Some(call_path) = semantic.resolve_call_path(func.as_ref()) { + if let Some(qualified_name) = semantic.resolve_qualified_name(func.as_ref()) { if matches!( - call_path.segments(), + qualified_name.segments(), ["django", "utils", "module_loading", "import_string"] ) { if let Some(argument) = arguments.find_argument("dotted_path", 0) { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs index 1e1b28378da89..26242791398b0 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/lambda_assignment.rs @@ -153,10 +153,15 @@ fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec bool { match expr { // Ex) `np.dtype(obj)` Expr::Call(ast::ExprCall { func, .. }) => semantic - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["numpy", "dtype"])), + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["numpy", "dtype"])), // Ex) `obj.dtype` Expr::Attribute(ast::ExprAttribute { attr, .. }) => { // Ex) `obj.dtype` diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 4f69381e99a85..9ce0a757ac58b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -1,7 +1,7 @@ -use ruff_python_ast::call_path::CallPath; use std::collections::BTreeSet; use ruff_python_ast::helpers::map_callable; +use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::{Definition, SemanticModel}; use ruff_source_file::UniversalNewlines; @@ -53,11 +53,11 @@ pub(crate) fn should_ignore_definition( function.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| { ignore_decorators .iter() - .any(|decorator| CallPath::from_qualified_name(decorator) == call_path) + .any(|decorator| QualifiedName::from_dotted_name(decorator) == qualified_name) }) }) } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs index 0b03a100aae5a..3a2576775b386 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/non_imperative_mood.rs @@ -5,7 +5,7 @@ use once_cell::sync::Lazy; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_python_semantic::analyze::visibility::{is_property, is_test}; use ruff_source_file::UniversalNewlines; use ruff_text_size::Ranged; @@ -74,8 +74,8 @@ pub(crate) fn non_imperative_mood( let property_decorators = property_decorators .iter() - .map(|decorator| CallPath::from_qualified_name(decorator)) - .collect::>(); + .map(|decorator| QualifiedName::from_dotted_name(decorator)) + .collect::>(); if is_test(&function.name) || is_property( diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs index 2817705458eaf..d60bb37deb7a7 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_open_mode.rs @@ -90,8 +90,10 @@ fn is_open(func: &Expr, semantic: &SemanticModel) -> Option { Expr::Attribute(ast::ExprAttribute { attr, value, .. }) if attr.as_str() == "open" => { match value.as_ref() { Expr::Call(ast::ExprCall { func, .. }) => semantic - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pathlib", "Path"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pathlib", "Path"]) + }) .then_some(Kind::Pathlib), _ => None, } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs index 8e649b849ef47..1378162b9cf57 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_default.rs @@ -50,8 +50,8 @@ pub(crate) fn invalid_envvar_default(checker: &mut Checker, call: &ast::ExprCall if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["os", "getenv"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "getenv"])) { // Find the `default` argument, if it exists. let Some(expr) = call.arguments.find_argument("default", 1) else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs index 91bdd5f44af81..d174137bbc667 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_envvar_value.rs @@ -43,8 +43,8 @@ pub(crate) fn invalid_envvar_value(checker: &mut Checker, call: &ast::ExprCall) if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["os", "getenv"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["os", "getenv"])) { // Find the `key` argument, if it exists. let Some(expr) = call.arguments.find_argument("key", 0) else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/logging.rs b/crates/ruff_linter/src/rules/pylint/rules/logging.rs index cfa3cbc6ce61c..36ad2af0fd551 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/logging.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/logging.rs @@ -115,10 +115,13 @@ pub(crate) fn logging_call(checker: &mut Checker, call: &ast::ExprCall) { } } Expr::Name(_) => { - let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else { + let Some(qualified_name) = checker + .semantic() + .resolve_qualified_name(call.func.as_ref()) + else { return; }; - let ["logging", attribute] = call_path.segments() else { + let ["logging", attribute] = qualified_name.segments() else { return; }; if LoggingLevel::from_attribute(attribute).is_none() { diff --git a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs index dbed3d43bb9eb..65291246fff0e 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/no_self_use.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; use ruff_python_ast::identifier::Identifier; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, ParameterWithDefault}; use ruff_python_semantic::{ analyze::{function_type, visibility}, @@ -84,8 +84,8 @@ pub(crate) fn no_self_use( .pydocstyle .property_decorators .iter() - .map(|decorator| CallPath::from_qualified_name(decorator)) - .collect::>(); + .map(|decorator| QualifiedName::from_dotted_name(decorator)) + .collect::>(); if helpers::is_empty(body) || visibility::is_magic(name) diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs index 876237b04bfec..2371a9b30327c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_slot_assignment.rs @@ -66,8 +66,8 @@ pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtCl if !class_def.bases().iter().all(|base| { checker .semantic() - .resolve_call_path(base) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "object"])) + .resolve_qualified_name(base) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "object"])) }) { return; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs index bf088aeff0e13..2636ad985f67a 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs @@ -202,9 +202,14 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr, semantic: &SemanticModel) -> // Ignore `sys.version_info` and `sys.platform` comparisons, which are only // respected by type checkers when enforced via equality. if any_over_expr(value, &|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"] + ) + }) }) { return false; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs index 8a2a59c4e6ce5..ec732781e3b85 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs @@ -96,9 +96,9 @@ pub(crate) fn singledispatch_method( for decorator in decorator_list { if checker .semantic() - .resolve_call_path(&decorator.expression) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["functools", "singledispatch"]) + .resolve_qualified_name(&decorator.expression) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["functools", "singledispatch"]) }) { let mut diagnostic = Diagnostic::new(SingledispatchMethod, decorator.range()); diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs index 3e82a75a7d3cf..1815d8e66b518 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_popen_preexec_fn.rs @@ -57,8 +57,8 @@ pub(crate) fn subprocess_popen_preexec_fn(checker: &mut Checker, call: &ast::Exp if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "Popen"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["subprocess", "Popen"])) { if let Some(keyword) = call .arguments diff --git a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs index c64568d2ecfa8..c6ff569fcbd06 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/subprocess_run_without_check.rs @@ -67,8 +67,8 @@ pub(crate) fn subprocess_run_without_check(checker: &mut Checker, call: &ast::Ex if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "run"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["subprocess", "run"])) { if call.arguments.find_keyword("check").is_none() { let mut diagnostic = Diagnostic::new(SubprocessRunWithoutCheck, call.func.range()); diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs index 4f95f0f603e47..2fd82e175d751 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs @@ -101,24 +101,25 @@ pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { }; if is_const_true(covariant) && is_const_true(contravariant) { - let Some(kind) = checker - .semantic() - .resolve_call_path(func) - .and_then(|call_path| { - if checker - .semantic() - .match_typing_call_path(&call_path, "ParamSpec") - { - Some(VarKind::ParamSpec) - } else if checker - .semantic() - .match_typing_call_path(&call_path, "TypeVar") - { - Some(VarKind::TypeVar) - } else { - None - } - }) + let Some(kind) = + checker + .semantic() + .resolve_qualified_name(func) + .and_then(|qualified_name| { + if checker + .semantic() + .match_typing_qualified_name(&qualified_name, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_qualified_name(&qualified_name, "TypeVar") + { + Some(VarKind::TypeVar) + } else { + None + } + }) else { return; }; diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs index 1995eff0f58d1..75a86035d398d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -95,16 +95,16 @@ pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) let Some(kind) = checker .semantic() - .resolve_call_path(func) - .and_then(|call_path| { + .resolve_qualified_name(func) + .and_then(|qualified_name| { if checker .semantic() - .match_typing_call_path(&call_path, "ParamSpec") + .match_typing_qualified_name(&qualified_name, "ParamSpec") { Some(VarKind::ParamSpec) } else if checker .semantic() - .match_typing_call_path(&call_path, "TypeVar") + .match_typing_qualified_name(&qualified_name, "TypeVar") { Some(VarKind::TypeVar) } else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs index cad3db6de4b55..805f98e97c676 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -89,26 +89,26 @@ pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targ let Some(kind) = checker .semantic() - .resolve_call_path(func) - .and_then(|call_path| { + .resolve_qualified_name(func) + .and_then(|qualified_name| { if checker .semantic() - .match_typing_call_path(&call_path, "ParamSpec") + .match_typing_qualified_name(&qualified_name, "ParamSpec") { Some(VarKind::ParamSpec) } else if checker .semantic() - .match_typing_call_path(&call_path, "TypeVar") + .match_typing_qualified_name(&qualified_name, "TypeVar") { Some(VarKind::TypeVar) } else if checker .semantic() - .match_typing_call_path(&call_path, "TypeVarTuple") + .match_typing_qualified_name(&qualified_name, "TypeVarTuple") { Some(VarKind::TypeVarTuple) } else if checker .semantic() - .match_typing_call_path(&call_path, "NewType") + .match_typing_qualified_name(&qualified_name, "NewType") { Some(VarKind::NewType) } else { diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs index c72e43f585ca9..67cf9d7469109 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_list_index_lookup.rs @@ -126,8 +126,10 @@ fn enumerate_items<'a>( // Check that the function is the `enumerate` builtin. if !semantic - .resolve_call_path(func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["builtins" | "", "enumerate"])) + .resolve_qualified_name(func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["builtins" | "", "enumerate"]) + }) { return None; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index ae44de7828dda..fabd85f18e382 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -3,7 +3,7 @@ use anyhow::Result; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::Expr; use ruff_text_size::{Ranged, TextRange}; @@ -72,9 +72,9 @@ impl AlwaysFixableViolation for UnspecifiedEncoding { pub(crate) fn unspecified_encoding(checker: &mut Checker, call: &ast::ExprCall) { let Some((function_name, mode)) = checker .semantic() - .resolve_call_path(&call.func) - .filter(|call_path| is_violation(call, call_path)) - .map(|call_path| (call_path.to_string(), Mode::from(&call_path))) + .resolve_qualified_name(&call.func) + .filter(|qualified_name| is_violation(call, qualified_name)) + .map(|qualified_name| (qualified_name.to_string(), Mode::from(&qualified_name))) else { return; }; @@ -145,7 +145,7 @@ fn is_binary_mode(expr: &Expr) -> Option { } /// Returns `true` if the given call lacks an explicit `encoding`. -fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool { +fn is_violation(call: &ast::ExprCall, qualified_name: &QualifiedName) -> bool { // If we have something like `*args`, which might contain the encoding argument, abort. if call.arguments.args.iter().any(Expr::is_starred_expr) { return false; @@ -159,7 +159,7 @@ fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool { { return false; } - match call_path.segments() { + match qualified_name.segments() { ["" | "codecs" | "_io", "open"] => { if let Some(mode_arg) = call.arguments.find_argument("mode", 1) { if is_binary_mode(mode_arg).unwrap_or(true) { @@ -171,7 +171,7 @@ fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool { call.arguments.find_argument("encoding", 3).is_none() } ["tempfile", "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"] => { - let mode_pos = usize::from(call_path.segments()[1] == "SpooledTemporaryFile"); + let mode_pos = usize::from(qualified_name.segments()[1] == "SpooledTemporaryFile"); if let Some(mode_arg) = call.arguments.find_argument("mode", mode_pos) { if is_binary_mode(mode_arg).unwrap_or(true) { // binary mode or unknown mode is no violation @@ -198,8 +198,8 @@ enum Mode { Unsupported, } -impl From<&CallPath<'_>> for Mode { - fn from(value: &CallPath<'_>) -> Self { +impl From<&QualifiedName<'_>> for Mode { + fn from(value: &QualifiedName<'_>) -> Self { match value.segments() { ["" | "codecs" | "_io", "open"] => Mode::Supported, ["tempfile", "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"] => { diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs index 2179278f78266..1df8a4ab25358 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_exception_statement.rs @@ -63,33 +63,35 @@ pub(crate) fn useless_exception_statement(checker: &mut Checker, expr: &ast::Stm /// /// See: fn is_builtin_exception(expr: &Expr, semantic: &SemanticModel) -> bool { - return semantic.resolve_call_path(expr).is_some_and(|call_path| { - matches!( - call_path.segments(), - [ - "", - "SystemExit" - | "Exception" - | "ArithmeticError" - | "AssertionError" - | "AttributeError" - | "BufferError" - | "EOFError" - | "ImportError" - | "LookupError" - | "IndexError" - | "KeyError" - | "MemoryError" - | "NameError" - | "ReferenceError" - | "RuntimeError" - | "NotImplementedError" - | "StopIteration" - | "SyntaxError" - | "SystemError" - | "TypeError" - | "ValueError" - ] - ) - }); + return semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + [ + "", + "SystemExit" + | "Exception" + | "ArithmeticError" + | "AssertionError" + | "AttributeError" + | "BufferError" + | "EOFError" + | "ImportError" + | "LookupError" + | "IndexError" + | "KeyError" + | "MemoryError" + | "NameError" + | "ReferenceError" + | "RuntimeError" + | "NotImplementedError" + | "StopIteration" + | "SyntaxError" + | "SystemError" + | "TypeError" + | "ValueError" + ] + ) + }); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs index e4a3fccf206c5..9d949c9ca7762 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_with_lock.rs @@ -65,10 +65,10 @@ pub(crate) fn useless_with_lock(checker: &mut Checker, with: &ast::StmtWith) { if !checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| { + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "threading", "Lock" | "RLock" | "Condition" | "Semaphore" | "BoundedSemaphore" diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs index db911c1c59c18..8af75bc012f1a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/datetime_utc_alias.rs @@ -53,8 +53,10 @@ impl Violation for DatetimeTimezoneUTC { pub(crate) fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["datetime", "timezone", "utc"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["datetime", "timezone", "utc"]) + }) { let mut diagnostic = Diagnostic::new(DatetimeTimezoneUTC, expr.range()); diagnostic.try_set_fix(|| { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs index b87080dcd0d98..a4156b83e3538 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -7,7 +7,7 @@ use log::error; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::whitespace::indentation; use ruff_python_ast::{self as ast, Stmt}; use ruff_python_codegen::Stylist; @@ -255,8 +255,8 @@ pub(crate) fn deprecated_mock_attribute(checker: &mut Checker, attribute: &ast:: return; } - if CallPath::from_expr(&attribute.value) - .is_some_and(|call_path| matches!(call_path.segments(), ["mock", "mock"])) + if UnqualifiedName::from_expr(&attribute.value) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["mock", "mock"])) { let mut diagnostic = Diagnostic::new( DeprecatedMockImport { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs index 2d11683aa12f6..e813979276b2a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -465,10 +465,10 @@ pub(crate) fn f_strings( expr.as_call_expr().is_some_and(|call| { checker .semantic() - .resolve_call_path(call.func.as_ref()) - .map_or(false, |call_path| { + .resolve_qualified_name(call.func.as_ref()) + .map_or(false, |qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["django", "utils", "translation", "gettext" | "gettext_lazy"] ) }) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 36891ecf83bec..58317848dc241 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -76,8 +76,10 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: && keywords.len() == 1 && checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["functools", "lru_cache"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["functools", "lru_cache"]) + }) { let Keyword { arg, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 8bed334e119ce..69ac48e29dc3b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -69,8 +69,10 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list && arguments.keywords.is_empty() && checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["functools", "lru_cache"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["functools", "lru_cache"]) + }) { let mut diagnostic = Diagnostic::new( LRUCacheWithoutParameters, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs index ae0f4eb1a4d1d..685ddfcaaa22e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/open_alias.rs @@ -49,8 +49,8 @@ impl Violation for OpenAlias { pub(crate) fn open_alias(checker: &mut Checker, expr: &Expr, func: &Expr) { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["io", "open"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["io", "open"])) { let mut diagnostic = Diagnostic::new(OpenAlias, expr.range()); if checker.semantic().is_builtin("open") { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs index 67fb737911afc..c7a0567f06ac4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/os_error_alias.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::fix::edits::pad; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -56,27 +56,29 @@ impl AlwaysFixableViolation for OSErrorAlias { /// Return `true` if an [`Expr`] is an alias of `OSError`. fn is_alias(expr: &Expr, semantic: &SemanticModel) -> bool { - semantic.resolve_call_path(expr).is_some_and(|call_path| { - matches!( - call_path.segments(), - ["", "EnvironmentError" | "IOError" | "WindowsError"] - | ["mmap" | "select" | "socket" | "os", "error"] - ) - }) + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "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_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "OSError"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "OSError"])) } /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. fn atom_diagnostic(checker: &mut Checker, target: &Expr) { let mut diagnostic = Diagnostic::new( OSErrorAlias { - name: compose_call_path(target), + name: UnqualifiedName::from_expr(target).map(|name| name.to_string()), }, target.range(), ); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index de4e8df6d0c30..04391303d2caa 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -97,8 +97,10 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) { // Detect `sys.version_info`, along with slices (like `sys.version_info[:2]`). if !checker .semantic() - .resolve_call_path(map_subscript(left)) - .is_some_and(|call_path| matches!(call_path.segments(), ["sys", "version_info"])) + .resolve_qualified_name(map_subscript(left)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["sys", "version_info"]) + }) { continue; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 8005d4f691a97..ec5ee0d2d5823 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -60,8 +60,8 @@ pub(crate) fn replace_stdout_stderr(checker: &mut Checker, call: &ast::ExprCall) if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "run"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["subprocess", "run"])) { // Find `stdout` and `stderr` kwargs. let Some(stdout) = call.arguments.find_keyword("stdout") else { @@ -74,12 +74,16 @@ pub(crate) fn replace_stdout_stderr(checker: &mut Checker, call: &ast::ExprCall) // Verify that they're both set to `subprocess.PIPE`. if !checker .semantic() - .resolve_call_path(&stdout.value) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "PIPE"])) + .resolve_qualified_name(&stdout.value) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["subprocess", "PIPE"]) + }) || !checker .semantic() - .resolve_call_path(&stderr.value) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "PIPE"])) + .resolve_qualified_name(&stderr.value) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["subprocess", "PIPE"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs index 96e5edf7f6c82..6638ae1086dae 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_universal_newlines.rs @@ -56,8 +56,8 @@ pub(crate) fn replace_universal_newlines(checker: &mut Checker, call: &ast::Expr if checker .semantic() - .resolve_call_path(&call.func) - .is_some_and(|call_path| matches!(call_path.segments(), ["subprocess", "run"])) + .resolve_qualified_name(&call.func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["subprocess", "run"])) { let Some(kwarg) = call.arguments.find_keyword("universal_newlines") else { return; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs index 6e177dd4619ef..b3591f26d78a6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/timeout_error_alias.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::fix::edits::pad; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_semantic::SemanticModel; use crate::checkers::ast::Checker; @@ -59,38 +59,40 @@ impl AlwaysFixableViolation for TimeoutErrorAlias { /// Return `true` if an [`Expr`] is an alias of `TimeoutError`. fn is_alias(expr: &Expr, semantic: &SemanticModel, target_version: PythonVersion) -> bool { - semantic.resolve_call_path(expr).is_some_and(|call_path| { - if target_version >= PythonVersion::Py311 { - matches!( - call_path.segments(), - ["socket", "timeout"] | ["asyncio", "TimeoutError"] - ) - } else { - // N.B. This lint is only invoked for Python 3.10+. We assume - // as much here since otherwise socket.timeout would be an unsafe - // fix in Python <3.10. We add an assert to make this assumption - // explicit. - assert!( - target_version >= PythonVersion::Py310, - "lint should only be used for Python 3.10+", - ); - matches!(call_path.segments(), ["socket", "timeout"]) - } - }) + semantic + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + if target_version >= PythonVersion::Py311 { + matches!( + qualified_name.segments(), + ["socket", "timeout"] | ["asyncio", "TimeoutError"] + ) + } else { + // N.B. This lint is only invoked for Python 3.10+. We assume + // as much here since otherwise socket.timeout would be an unsafe + // fix in Python <3.10. We add an assert to make this assumption + // explicit. + assert!( + target_version >= PythonVersion::Py310, + "lint should only be used for Python 3.10+", + ); + matches!(qualified_name.segments(), ["socket", "timeout"]) + } + }) } /// Return `true` if an [`Expr`] is `TimeoutError`. fn is_timeout_error(expr: &Expr, semantic: &SemanticModel) -> bool { semantic - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "TimeoutError"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "TimeoutError"])) } /// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`]. fn atom_diagnostic(checker: &mut Checker, target: &Expr) { let mut diagnostic = Diagnostic::new( TimeoutErrorAlias { - name: compose_call_path(target), + name: UnqualifiedName::from_expr(target).map(|name| name.to_string()), }, target.range(), ); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs index 072145caa06de..c2fc297eaf9ca 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -60,8 +60,8 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, }; if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "type"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "type"])) { return; } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index e939dd9eb9d1a..a186e8b3d0e23 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -53,8 +53,8 @@ pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| matches!(call_path.segments(), ["typing", "Text"])) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"])) { let mut diagnostic = Diagnostic::new(TypingTextStrAlias, expr.range()); if checker.semantic().is_builtin("str") { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index c963d140cee27..d47a6fc2e3e2e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -2,7 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; +use ruff_python_ast::name::UnqualifiedName; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; @@ -81,12 +81,12 @@ pub(crate) fn use_pep585_annotation( expr: &Expr, replacement: &ModuleMember, ) { - let Some(from) = compose_call_path(expr) else { + let Some(from) = UnqualifiedName::from_expr(expr) else { return; }; let mut diagnostic = Diagnostic::new( NonPEP585Annotation { - from, + from: from.to_string(), to: replacement.to_string(), }, expr.range(), diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 838c884f77210..671cca0f0bd71 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -100,8 +100,8 @@ pub(crate) fn bit_count(checker: &mut Checker, call: &ExprCall) { // Ensure that we're performing a `bin(...)`. if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["" | "builtins", "bin"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["" | "builtins", "bin"])) { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs index 12c23fa2c32cb..6f210e60827c7 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/hashlib_digest_hex.rs @@ -85,10 +85,10 @@ pub(crate) fn hashlib_digest_hex(checker: &mut Checker, call: &ExprCall) { if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| { + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "hashlib", "md5" diff --git a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs index 2e9e1a41590f5..7274e49795a61 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/implicit_cwd.rs @@ -76,8 +76,8 @@ pub(crate) fn no_implicit_cwd(checker: &mut Checker, call: &ExprCall) { if !checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["pathlib", "Path"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pathlib", "Path"])) { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs index 596316abcc21f..5982cdca7fd6d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs @@ -63,8 +63,8 @@ pub(crate) fn metaclass_abcmeta(checker: &mut Checker, class_def: &StmtClassDef) // Determine whether it's assigned to `abc.ABCMeta`. if !checker .semantic() - .resolve_call_path(&keyword.value) - .is_some_and(|call_path| matches!(call_path.segments(), ["abc", "ABCMeta"])) + .resolve_qualified_name(&keyword.value) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["abc", "ABCMeta"])) { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs index 5c5d1be7eeee6..6bc52c14e5484 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs @@ -72,9 +72,9 @@ impl Violation for PrintEmptyString { pub(crate) fn print_empty_string(checker: &mut Checker, call: &ast::ExprCall) { if !checker .semantic() - .resolve_call_path(&call.func) + .resolve_qualified_name(&call.func) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["", "print"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "print"])) { return; } diff --git a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs index 9249b8c36d26f..4fbc2774cf450 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/redundant_log_base.rs @@ -76,9 +76,9 @@ pub(crate) fn redundant_log_base(checker: &mut Checker, call: &ast::ExprCall) { if !checker .semantic() - .resolve_call_path(&call.func) + .resolve_qualified_name(&call.func) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["math", "log"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["math", "log"])) { return; } @@ -89,9 +89,9 @@ pub(crate) fn redundant_log_base(checker: &mut Checker, call: &ast::ExprCall) { Base::Ten } else if checker .semantic() - .resolve_call_path(base) + .resolve_qualified_name(base) .as_ref() - .is_some_and(|call_path| matches!(call_path.segments(), ["math", "e"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["math", "e"])) { Base::E } else { diff --git a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs index ab691ab78f9b7..81facd1417b2e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs @@ -57,21 +57,20 @@ pub(crate) fn regex_flag_alias(checker: &mut Checker, expr: &Expr) { return; } - let Some(flag) = - checker - .semantic() - .resolve_call_path(expr) - .and_then(|call_path| match call_path.segments() { - ["re", "A"] => Some(RegexFlag::Ascii), - ["re", "I"] => Some(RegexFlag::IgnoreCase), - ["re", "L"] => Some(RegexFlag::Locale), - ["re", "M"] => Some(RegexFlag::Multiline), - ["re", "S"] => Some(RegexFlag::DotAll), - ["re", "T"] => Some(RegexFlag::Template), - ["re", "U"] => Some(RegexFlag::Unicode), - ["re", "X"] => Some(RegexFlag::Verbose), - _ => None, - }) + let Some(flag) = checker + .semantic() + .resolve_qualified_name(expr) + .and_then(|qualified_name| match qualified_name.segments() { + ["re", "A"] => Some(RegexFlag::Ascii), + ["re", "I"] => Some(RegexFlag::IgnoreCase), + ["re", "L"] => Some(RegexFlag::Locale), + ["re", "M"] => Some(RegexFlag::Multiline), + ["re", "S"] => Some(RegexFlag::DotAll), + ["re", "T"] => Some(RegexFlag::Template), + ["re", "U"] => Some(RegexFlag::Unicode), + ["re", "X"] => Some(RegexFlag::Verbose), + _ => None, + }) else { return; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs index 58f2dbfeed692..4c04fc0d43af1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -71,14 +71,13 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op }; // Ex) `asyncio.create_task(...)` - if let Some(method) = - semantic - .resolve_call_path(func) - .and_then(|call_path| match call_path.segments() { - ["asyncio", "create_task"] => Some(Method::CreateTask), - ["asyncio", "ensure_future"] => Some(Method::EnsureFuture), - _ => None, - }) + if let Some(method) = semantic + .resolve_qualified_name(func) + .and_then(|qualified_name| match qualified_name.segments() { + ["asyncio", "create_task"] => Some(Method::CreateTask), + ["asyncio", "ensure_future"] => Some(Method::EnsureFuture), + _ => None, + }) { return Some(Diagnostic::new( AsyncioDanglingTask { @@ -93,9 +92,9 @@ pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Op if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() { if attr == "create_task" { if let Expr::Name(name) = value.as_ref() { - if typing::resolve_assignment(value, semantic).is_some_and(|call_path| { + if typing::resolve_assignment(value, semantic).is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "asyncio", "get_event_loop" | "get_running_loop" | "new_event_loop" diff --git a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs index c4324792dc091..61adfd48dd469 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/default_factory_kwarg.rs @@ -77,8 +77,10 @@ pub(crate) fn default_factory_kwarg(checker: &mut Checker, call: &ast::ExprCall) // If the call isn't a `defaultdict` constructor, return. if !checker .semantic() - .resolve_call_path(call.func.as_ref()) - .is_some_and(|call_path| matches!(call_path.segments(), ["collections", "defaultdict"])) + .resolve_qualified_name(call.func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["collections", "defaultdict"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs index 7bf343b695759..017e243da93f1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/function_call_in_dataclass_default.rs @@ -2,8 +2,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::compose_call_path; -use ruff_python_ast::call_path::CallPath; +use ruff_python_ast::name::{QualifiedName, UnqualifiedName}; use ruff_python_semantic::analyze::typing::is_immutable_func; use ruff_text_size::Ranged; @@ -80,12 +79,12 @@ pub(crate) fn function_call_in_dataclass_default( return; } - let extend_immutable_calls: Vec = checker + let extend_immutable_calls: Vec = checker .settings .flake8_bugbear .extend_immutable_calls .iter() - .map(|target| CallPath::from_qualified_name(target)) + .map(|target| QualifiedName::from_dotted_name(target)) .collect(); for statement in &class_def.body { @@ -103,7 +102,7 @@ pub(crate) fn function_call_in_dataclass_default( { checker.diagnostics.push(Diagnostic::new( FunctionCallInDataclassDefaultArgument { - name: compose_call_path(func), + name: UnqualifiedName::from_expr(func).map(|name| name.to_string()), }, expr.range(), )); diff --git a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs index 7025ab30765b2..e151a5bb16013 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs @@ -25,8 +25,8 @@ pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool } semantic - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["dataclasses", "field"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"])) } /// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. @@ -59,8 +59,10 @@ pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticMod class_def.decorator_list.iter().any(|decorator| { semantic - .resolve_call_path(map_callable(&decorator.expression)) - .is_some_and(|call_path| matches!(call_path.segments(), ["dataclasses", "dataclass"])) + .resolve_qualified_name(map_callable(&decorator.expression)) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["dataclasses", "dataclass"]) + }) }) } @@ -72,9 +74,9 @@ pub(super) fn has_default_copy_semantics( class_def: &ast::StmtClassDef, semantic: &SemanticModel, ) -> bool { - analyze::class::any_call_path(class_def, semantic, &|call_path| { + analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"] | ["pydantic_settings", "BaseSettings"] | ["msgspec", "Struct"] diff --git a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs index 45abda9067641..f68983834e252 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/never_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/never_union.rs @@ -193,10 +193,10 @@ enum NeverLike { impl NeverLike { fn from_expr(expr: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> Option { - let call_path = semantic.resolve_call_path(expr)?; - if semantic.match_typing_call_path(&call_path, "NoReturn") { + let qualified_name = semantic.resolve_qualified_name(expr)?; + if semantic.match_typing_qualified_name(&qualified_name, "NoReturn") { Some(NeverLike::NoReturn) - } else if semantic.match_typing_call_path(&call_path, "Never") { + } else if semantic.match_typing_qualified_name(&qualified_name, "Never") { Some(NeverLike::Never) } else { None diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs index deeb339f0c8eb..286f162b4c893 100644 --- a/crates/ruff_linter/src/rules/ruff/typing.rs +++ b/crates/ruff_linter/src/rules/ruff/typing.rs @@ -1,18 +1,18 @@ use itertools::Either::{Left, Right}; +use ruff_python_ast::name::QualifiedName; use ruff_python_ast::{self as ast, Expr, Operator}; -use ruff_python_ast::call_path::CallPath; use ruff_python_parser::typing::parse_type_annotation; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::sys::is_known_standard_library; use ruff_source_file::Locator; -/// Returns `true` if the given call path is a known type. +/// Returns `true` if the given qualified name is a known type. /// /// A known type is either a builtin type, any object from the standard library, /// or a type from the `typing_extensions` module. -fn is_known_type(call_path: &CallPath, minor_version: u8) -> bool { - match call_path.segments() { +fn is_known_type(qualified_name: &QualifiedName, minor_version: u8) -> bool { + match qualified_name.segments() { ["" | "typing_extensions", ..] => true, [module, ..] => is_known_standard_library(minor_version, module), _ => false, @@ -79,23 +79,24 @@ impl<'a> TypingTarget<'a> { ) -> Option { match expr { Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { - semantic.resolve_call_path(value).map_or( + semantic.resolve_qualified_name(value).map_or( // If we can't resolve the call path, it must be defined // in the same file and could be a type alias. Some(TypingTarget::Unknown), - |call_path| { - if semantic.match_typing_call_path(&call_path, "Optional") { + |qualified_name| { + if semantic.match_typing_qualified_name(&qualified_name, "Optional") { Some(TypingTarget::Optional(slice.as_ref())) - } else if semantic.match_typing_call_path(&call_path, "Literal") { + } else if semantic.match_typing_qualified_name(&qualified_name, "Literal") { Some(TypingTarget::Literal(slice.as_ref())) - } else if semantic.match_typing_call_path(&call_path, "Union") { + } else if semantic.match_typing_qualified_name(&qualified_name, "Union") { Some(TypingTarget::Union(slice.as_ref())) - } else if semantic.match_typing_call_path(&call_path, "Annotated") { + } else if semantic.match_typing_qualified_name(&qualified_name, "Annotated") + { resolve_slice_value(slice.as_ref()) .next() .map(TypingTarget::Annotated) } else { - if is_known_type(&call_path, minor_version) { + if is_known_type(&qualified_name, minor_version) { Some(TypingTarget::Known) } else { Some(TypingTarget::Unknown) @@ -115,20 +116,23 @@ impl<'a> TypingTarget<'a> { parse_type_annotation(value.to_str(), *range, locator.contents()) .map_or(None, |(expr, _)| Some(TypingTarget::ForwardReference(expr))) } - _ => semantic.resolve_call_path(expr).map_or( + _ => semantic.resolve_qualified_name(expr).map_or( // If we can't resolve the call path, it must be defined in the // same file, so we assume it's `Any` as it could be a type alias. Some(TypingTarget::Unknown), - |call_path| { - if semantic.match_typing_call_path(&call_path, "Any") { + |qualified_name| { + if semantic.match_typing_qualified_name(&qualified_name, "Any") { Some(TypingTarget::Any) - } else if matches!(call_path.segments(), ["" | "builtins", "object"]) { + } else if matches!(qualified_name.segments(), ["" | "builtins", "object"]) { Some(TypingTarget::Object) - } else if semantic.match_typing_call_path(&call_path, "Hashable") - || matches!(call_path.segments(), ["collections", "abc", "Hashable"]) + } else if semantic.match_typing_qualified_name(&qualified_name, "Hashable") + || matches!( + qualified_name.segments(), + ["collections", "abc", "Hashable"] + ) { Some(TypingTarget::Hashable) - } else if !is_known_type(&call_path, minor_version) { + } else if !is_known_type(&qualified_name, minor_version) { // If it's not a known type, we assume it's `Any`. Some(TypingTarget::Unknown) } else { @@ -289,31 +293,30 @@ pub(crate) fn type_hint_resolves_to_any( #[cfg(test)] mod tests { - use ruff_python_ast::call_path::CallPath; - use super::is_known_type; + use ruff_python_ast::name::QualifiedName; #[test] fn test_is_known_type() { - assert!(is_known_type(&CallPath::from_slice(&["", "int"]), 11)); + assert!(is_known_type(&QualifiedName::from_slice(&["", "int"]), 11)); assert!(is_known_type( - &CallPath::from_slice(&["builtins", "int"]), + &QualifiedName::from_slice(&["builtins", "int"]), 11 )); assert!(is_known_type( - &CallPath::from_slice(&["typing", "Optional"]), + &QualifiedName::from_slice(&["typing", "Optional"]), 11 )); assert!(is_known_type( - &CallPath::from_slice(&["typing_extensions", "Literal"]), + &QualifiedName::from_slice(&["typing_extensions", "Literal"]), 11 )); assert!(is_known_type( - &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + &QualifiedName::from_slice(&["zoneinfo", "ZoneInfo"]), 11 )); assert!(!is_known_type( - &CallPath::from_slice(&["zoneinfo", "ZoneInfo"]), + &QualifiedName::from_slice(&["zoneinfo", "ZoneInfo"]), 8 )); } diff --git a/crates/ruff_linter/src/rules/tryceratops/helpers.rs b/crates/ruff_linter/src/rules/tryceratops/helpers.rs index 5889a1324a71a..7b07240c20a39 100644 --- a/crates/ruff_linter/src/rules/tryceratops/helpers.rs +++ b/crates/ruff_linter/src/rules/tryceratops/helpers.rs @@ -35,8 +35,10 @@ impl<'a, 'b> Visitor<'b> for LoggerCandidateVisitor<'a, 'b> { } } Expr::Name(_) => { - if let Some(call_path) = self.semantic.resolve_call_path(call.func.as_ref()) { - if let ["logging", attribute] = call_path.segments() { + if let Some(qualified_name) = + self.semantic.resolve_qualified_name(call.func.as_ref()) + { + if let ["logging", attribute] = qualified_name.segments() { if let Some(logging_level) = LoggingLevel::from_attribute(attribute) { { self.calls.push((call, logging_level)); diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs index 474947402ac0d..b98c71452a9f8 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/error_instead_of_exception.rs @@ -90,9 +90,9 @@ pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Exce // the object _may_ not be a logger. if checker .semantic() - .resolve_call_path(expr.func.as_ref()) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["logging", "error"]) + .resolve_qualified_name(expr.func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["logging", "error"]) }) { Applicability::Safe @@ -117,9 +117,12 @@ pub(crate) fn error_instead_of_exception(checker: &mut Checker, handlers: &[Exce // the object _may_ not be a logger. if checker .semantic() - .resolve_call_path(expr.func.as_ref()) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["logging", "error"]) + .resolve_qualified_name(expr.func.as_ref()) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["logging", "error"] + ) }) { Applicability::Safe diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs index 1222e881ffb90..561f2f9577599 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -72,8 +72,10 @@ pub(crate) fn raise_vanilla_args(checker: &mut Checker, expr: &Expr) { // `NotImplementedError`. if checker .semantic() - .resolve_call_path(func) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "NotImplementedError"])) + .resolve_qualified_name(func) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["", "NotImplementedError"]) + }) { return; } diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs index 6ca4d8d3798b1..bacdeeb12ef49 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_vanilla_class.rs @@ -65,12 +65,12 @@ impl Violation for RaiseVanillaClass { pub(crate) fn raise_vanilla_class(checker: &mut Checker, expr: &Expr) { if checker .semantic() - .resolve_call_path(if let Expr::Call(ast::ExprCall { func, .. }) = expr { + .resolve_qualified_name(if let Expr::Call(ast::ExprCall { func, .. }) = expr { func } else { expr }) - .is_some_and(|call_path| matches!(call_path.segments(), ["", "Exception"])) + .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["", "Exception"])) { checker .diagnostics diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs index 241b1e35d24cc..ca769add40ad1 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/raise_within_try.rs @@ -111,9 +111,12 @@ pub(crate) fn raise_within_try(checker: &mut Checker, body: &[Stmt], handlers: & || handled_exceptions.iter().any(|expr| { checker .semantic() - .resolve_call_path(expr) - .is_some_and(|call_path| { - matches!(call_path.segments(), ["", "Exception" | "BaseException"]) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "Exception" | "BaseException"] + ) }) }) { diff --git a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs index 72c91778f301c..9c326349138a4 100644 --- a/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs +++ b/crates/ruff_linter/src/rules/tryceratops/rules/type_check_without_type_error.rs @@ -75,10 +75,10 @@ fn has_control_flow(stmt: &Stmt) -> bool { fn check_type_check_call(checker: &mut Checker, call: &Expr) -> bool { checker .semantic() - .resolve_call_path(call) - .is_some_and(|call_path| { + .resolve_qualified_name(call) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), ["", "isinstance" | "issubclass" | "callable"] ) }) @@ -100,10 +100,10 @@ fn check_type_check_test(checker: &mut Checker, test: &Expr) -> bool { fn is_builtin_exception(checker: &mut Checker, exc: &Expr) -> bool { return checker .semantic() - .resolve_call_path(exc) - .is_some_and(|call_path| { + .resolve_qualified_name(exc) + .is_some_and(|qualified_name| { matches!( - call_path.segments(), + qualified_name.segments(), [ "", "ArithmeticError" diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 3b72ed907e48e..18b3e5cc9676a 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -7,7 +7,7 @@ use ruff_python_trivia::{indentation_at_offset, CommentRanges, SimpleTokenKind, use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::call_path::{CallPath, CallPathBuilder}; +use crate::name::{QualifiedName, QualifiedNameBuilder}; use crate::parenthesize::parenthesized_range; use crate::statement_visitor::StatementVisitor; use crate::visitor::Visitor; @@ -800,8 +800,8 @@ pub fn collect_import_from_member<'a>( level: Option, module: Option<&'a str>, member: &'a str, -) -> CallPath<'a> { - let mut call_path_builder = CallPathBuilder::with_capacity( +) -> QualifiedName<'a> { + let mut qualified_name_builder = QualifiedNameBuilder::with_capacity( level.unwrap_or_default() as usize + module .map(|module| module.split('.').count()) @@ -813,20 +813,20 @@ pub fn collect_import_from_member<'a>( if let Some(level) = level { if level > 0 { for _ in 0..level { - call_path_builder.push("."); + qualified_name_builder.push("."); } } } // Add the remaining segments. if let Some(module) = module { - call_path_builder.extend(module.split('.')); + qualified_name_builder.extend(module.split('.')); } // Add the member. - call_path_builder.push(member); + qualified_name_builder.push(member); - call_path_builder.build() + qualified_name_builder.build() } /// Format the call path for a relative import, or `None` if the relative import extends beyond @@ -838,29 +838,29 @@ pub fn from_relative_import<'a>( import: &[&'a str], // The remaining segments to the call path (e.g., given `bar.baz`, `["baz"]`). tail: &[&'a str], -) -> Option> { - let mut call_path_builder = - CallPathBuilder::with_capacity(module.len() + import.len() + tail.len()); +) -> Option> { + let mut qualified_name_builder = + QualifiedNameBuilder::with_capacity(module.len() + import.len() + tail.len()); // Start with the module path. - call_path_builder.extend(module.iter().map(String::as_str)); + qualified_name_builder.extend(module.iter().map(String::as_str)); // Remove segments based on the number of dots. for segment in import { if *segment == "." { - if call_path_builder.is_empty() { + if qualified_name_builder.is_empty() { return None; } - call_path_builder.pop(); + qualified_name_builder.pop(); } else { - call_path_builder.push(segment); + qualified_name_builder.push(segment); } } // Add the remaining segments. - call_path_builder.extend_from_slice(tail); + qualified_name_builder.extend_from_slice(tail); - Some(call_path_builder.build()) + Some(qualified_name_builder.build()) } /// Given an imported module (based on its relative import level and module name), return the diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 1fb53dc8c7bf6..52729e19818d9 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -6,7 +6,6 @@ pub use node::{AnyNode, AnyNodeRef, AstNode, NodeKind}; pub use nodes::*; pub mod all; -pub mod call_path; pub mod comparable; pub mod docstrings; mod expression; @@ -15,6 +14,7 @@ pub mod helpers; pub mod identifier; pub mod imports; mod int; +pub mod name; mod node; mod nodes; pub mod parenthesize; diff --git a/crates/ruff_python_ast/src/call_path.rs b/crates/ruff_python_ast/src/name.rs similarity index 73% rename from crates/ruff_python_ast/src/call_path.rs rename to crates/ruff_python_ast/src/name.rs index 0ad5aad711c66..0a1ff2837f0ac 100644 --- a/crates/ruff_python_ast/src/call_path.rs +++ b/crates/ruff_python_ast/src/name.rs @@ -5,49 +5,44 @@ use crate::{nodes, Expr}; /// A representation of a qualified name, like `typing.List`. #[derive(Debug, Clone, Eq, Hash)] -pub struct CallPath<'a> { +pub struct QualifiedName<'a> { segments: SmallVec<[&'a str; 8]>, } -impl<'a> CallPath<'a> { - pub fn from_expr(expr: &'a Expr) -> Option { - let segments = collect_call_path(expr)?; - Some(Self { segments }) - } - - /// Create a [`CallPath`] from an unqualified name. - /// - /// ```rust - /// # use smallvec::smallvec; - /// # use ruff_python_ast::call_path::CallPath; - /// - /// assert_eq!(CallPath::from_unqualified_name("typing.List").segments(), ["typing", "List"]); - /// assert_eq!(CallPath::from_unqualified_name("list").segments(), ["list"]); - /// ``` - #[inline] - pub fn from_unqualified_name(name: &'a str) -> Self { - name.split('.').collect() - } - - /// Create a [`CallPath`] from a fully-qualified name. +impl<'a> QualifiedName<'a> { + /// Create a [`QualifiedName`] from a dotted name. /// /// ```rust /// # use smallvec::smallvec; - /// # use ruff_python_ast::call_path::CallPath; + /// # use ruff_python_ast::name::QualifiedName; /// - /// assert_eq!(CallPath::from_qualified_name("typing.List").segments(), ["typing", "List"]); - /// assert_eq!(CallPath::from_qualified_name("list").segments(), ["", "list"]); + /// assert_eq!(QualifiedName::from_dotted_name("typing.List").segments(), ["typing", "List"]); + /// assert_eq!(QualifiedName::from_dotted_name("list").segments(), ["", "list"]); /// ``` #[inline] - pub fn from_qualified_name(name: &'a str) -> Self { + pub fn from_dotted_name(name: &'a str) -> Self { if let Some(dot) = name.find('.') { let mut segments = SmallVec::new(); segments.push(&name[..dot]); segments.extend(name[dot + 1..].split('.')); Self { segments } } else { - // Special-case: for builtins, return `["", "int"]` instead of `["int"]`. - Self::from_slice(&["", name]) + Self::builtin(name) + } + } + + /// Creates a name that's guaranteed not be a built in + #[inline] + pub fn imported(name: &'a str) -> Self { + name.split('.').collect() + } + + /// Creates a qualified name for a built in + #[inline] + pub fn builtin(name: &'a str) -> Self { + debug_assert!(!name.contains('.')); + Self { + segments: ["", name].into_iter().collect(), } } @@ -58,7 +53,7 @@ impl<'a> CallPath<'a> { } } - pub fn starts_with(&self, other: &CallPath) -> bool { + pub fn starts_with(&self, other: &QualifiedName) -> bool { self.segments().starts_with(other.segments()) } @@ -73,7 +68,7 @@ impl<'a> CallPath<'a> { } } -impl<'a> FromIterator<&'a str> for CallPath<'a> { +impl<'a> FromIterator<&'a str> for QualifiedName<'a> { fn from_iter>(iter: I) -> Self { Self { segments: iter.into_iter().collect(), @@ -81,32 +76,28 @@ impl<'a> FromIterator<&'a str> for CallPath<'a> { } } -impl<'a, 'b> PartialEq> for CallPath<'a> { +impl<'a, 'b> PartialEq> for QualifiedName<'a> { #[inline] - fn eq(&self, other: &CallPath<'b>) -> bool { + fn eq(&self, other: &QualifiedName<'b>) -> bool { self.segments == other.segments } } -#[derive(Debug, Clone, Default)] -pub struct CallPathBuilder<'a> { +#[derive(Debug, Clone)] +pub struct QualifiedNameBuilder<'a> { segments: SmallVec<[&'a str; 8]>, } -impl<'a> CallPathBuilder<'a> { +impl<'a> QualifiedNameBuilder<'a> { pub fn with_capacity(capacity: usize) -> Self { Self { segments: SmallVec::with_capacity(capacity), } } - pub fn new() -> Self { - Self::default() - } - - pub fn from_path(call_path: CallPath<'a>) -> Self { + pub fn from_qualified_name(qualified_name: QualifiedName<'a>) -> Self { Self { - segments: call_path.segments, + segments: qualified_name.segments, } } @@ -133,15 +124,115 @@ impl<'a> CallPathBuilder<'a> { self.segments.extend_from_slice(segments); } - pub fn build(self) -> CallPath<'a> { - CallPath { + pub fn build(self) -> QualifiedName<'a> { + QualifiedName { segments: self.segments, } } } -/// Convert an `Expr` to its [`CallPath`] segments (like `["typing", "List"]`). -fn collect_call_path(expr: &Expr) -> Option> { +impl Display for QualifiedName<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + format_qualified_name_segments(self.segments(), f) + } +} + +pub fn format_qualified_name_segments(segments: &[&str], w: &mut dyn Write) -> std::fmt::Result { + if segments.first().is_some_and(|first| first.is_empty()) { + // If the first segment is empty, the `CallPath` is that of a builtin. + // Ex) `["", "bool"]` -> `"bool"` + let mut first = true; + + for segment in segments.iter().skip(1) { + if !first { + w.write_char('.')?; + } + + w.write_str(segment)?; + first = false; + } + } else if segments.first().is_some_and(|first| matches!(*first, ".")) { + // If the call path is dot-prefixed, it's an unresolved relative import. + // Ex) `[".foo", "bar"]` -> `".foo.bar"` + + let mut iter = segments.iter(); + for segment in iter.by_ref() { + if *segment == "." { + w.write_char('.')?; + } else { + w.write_str(segment)?; + break; + } + } + for segment in iter { + w.write_char('.')?; + w.write_str(segment)?; + } + } else { + let mut first = true; + for segment in segments { + if !first { + w.write_char('.')?; + } + + w.write_str(segment)?; + first = false; + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Eq, Hash)] +pub struct UnqualifiedName<'a> { + segments: SmallVec<[&'a str; 8]>, +} + +impl<'a> UnqualifiedName<'a> { + pub fn from_expr(expr: &'a Expr) -> Option { + let segments = collect_segments(expr)?; + Some(Self { segments }) + } + + pub fn segments(&self) -> &[&'a str] { + &self.segments + } +} + +impl<'a, 'b> PartialEq> for UnqualifiedName<'a> { + #[inline] + fn eq(&self, other: &UnqualifiedName<'b>) -> bool { + self.segments == other.segments + } +} + +impl Display for UnqualifiedName<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for segment in &self.segments { + if !first { + f.write_char('.')?; + } + + f.write_str(segment)?; + first = false; + } + + Ok(()) + } +} + +impl<'a> FromIterator<&'a str> for UnqualifiedName<'a> { + #[inline] + fn from_iter>(iter: T) -> Self { + Self { + segments: iter.into_iter().collect(), + } + } +} + +/// Convert an `Expr` to its [`QualifiedName`] segments (like `["typing", "List"]`). +fn collect_segments(expr: &Expr) -> Option> { // Unroll the loop up to eight times, to match the maximum number of expected attributes. // In practice, unrolling appears to give about a 4x speed-up on this hot path. let attr1 = match expr { @@ -255,7 +346,7 @@ fn collect_call_path(expr: &Expr) -> Option> { _ => return None, }; - collect_call_path(&attr8.value).map(|mut segments| { + collect_segments(&attr8.value).map(|mut segments| { segments.extend([ attr8.attr.as_str(), attr7.attr.as_str(), @@ -269,60 +360,3 @@ fn collect_call_path(expr: &Expr) -> Option> { segments }) } - -impl Display for CallPath<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - format_call_path_segments(self.segments(), f) - } -} - -/// Convert an `Expr` to its call path (like `List`, or `typing.List`). -pub fn compose_call_path(expr: &Expr) -> Option { - CallPath::from_expr(expr).map(|call_path| call_path.to_string()) -} - -pub fn format_call_path_segments(segments: &[&str], w: &mut dyn Write) -> std::fmt::Result { - if segments.first().is_some_and(|first| first.is_empty()) { - // If the first segment is empty, the `CallPath` is that of a builtin. - // Ex) `["", "bool"]` -> `"bool"` - let mut first = true; - - for segment in segments.iter().skip(1) { - if !first { - w.write_char('.')?; - } - - w.write_str(segment)?; - first = false; - } - } else if segments.first().is_some_and(|first| matches!(*first, ".")) { - // If the call path is dot-prefixed, it's an unresolved relative import. - // Ex) `[".foo", "bar"]` -> `".foo.bar"` - - let mut iter = segments.iter(); - for segment in iter.by_ref() { - if *segment == "." { - w.write_char('.')?; - } else { - w.write_str(segment)?; - break; - } - } - for segment in iter { - w.write_char('.')?; - w.write_str(segment)?; - } - } else { - let mut first = true; - for segment in segments { - if !first { - w.write_char('.')?; - } - - w.write_str(segment)?; - first = false; - } - } - - Ok(()) -} diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index bbc3a1e2bd3c6..5aaeebc9de8ad 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -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, ) -> 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; diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index e3c2ba768dd19..ff6d3ea560c57 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -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) diff --git a/crates/ruff_python_semantic/src/analyze/imports.rs b/crates/ruff_python_semantic/src/analyze/imports.rs index 10ea0d927bfce..162cb6e653a94 100644 --- a/crates/ruff_python_semantic/src/analyze/imports.rs +++ b/crates/ruff_python_semantic/src/analyze/imports.rs @@ -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"])) } diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index cc90f4107c9e8..d72b46d4e9ec8 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -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); } diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 1664ba50cf378..59ba701b800fb 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -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, extend_generics: &[String], ) -> Option { - 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 { 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 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> { +) -> Option> { 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, } diff --git a/crates/ruff_python_semantic/src/analyze/visibility.rs b/crates/ruff_python_semantic/src/analyze/visibility.rs index 2e129d265f055..99e107f3d8a3b 100644 --- a/crates/ruff_python_semantic/src/analyze/visibility.rs +++ b/crates/ruff_python_semantic/src/analyze/visibility.rs @@ -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; diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 4e2028907bdde..10b4d13439246 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, DerefMut}; use bitflags::bitflags; use ruff_index::{newtype_index, IndexSlice, IndexVec}; -use ruff_python_ast::call_path::format_call_path_segments; +use ruff_python_ast::name::format_qualified_name_segments; use ruff_python_ast::Stmt; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextRange}; @@ -125,38 +125,38 @@ impl<'a> Binding<'a> { // import foo.baz // ``` BindingKind::Import(Import { - call_path: redefinition, + qualified_name: redefinition, }) => { if let BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, + qualified_name: definition, }) = &existing.kind { return redefinition == definition; } } BindingKind::FromImport(FromImport { - call_path: redefinition, + qualified_name: redefinition, }) => { if let BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, + qualified_name: definition, }) = &existing.kind { return redefinition == definition; } } BindingKind::SubmoduleImport(SubmoduleImport { - call_path: redefinition, + qualified_name: redefinition, }) => match &existing.kind { BindingKind::Import(Import { - call_path: definition, + qualified_name: definition, }) | BindingKind::SubmoduleImport(SubmoduleImport { - call_path: definition, + qualified_name: definition, }) => { return redefinition == definition; } BindingKind::FromImport(FromImport { - call_path: definition, + qualified_name: definition, }) => { return redefinition == definition; } @@ -362,7 +362,7 @@ pub struct Import<'a> { /// The full name of the module being imported. /// Ex) Given `import foo`, `qualified_name` would be "foo". /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". - pub call_path: Box<[&'a str]>, + pub qualified_name: Box<[&'a str]>, } /// A binding for a member imported from a module, keyed on the name to which the member is bound. @@ -373,7 +373,7 @@ pub struct FromImport<'a> { /// The full name of the member being imported. /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". - pub call_path: Box<[&'a str]>, + pub qualified_name: Box<[&'a str]>, } /// A binding for a submodule imported from a module, keyed on the name of the parent module. @@ -382,7 +382,7 @@ pub struct FromImport<'a> { pub struct SubmoduleImport<'a> { /// The full name of the submodule being imported. /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". - pub call_path: Box<[&'a str]>, + pub qualified_name: Box<[&'a str]>, } #[derive(Debug, Clone, is_macro::Is)] @@ -561,7 +561,7 @@ pub trait Imported<'a> { /// Returns the fully-qualified name of the imported symbol. fn qualified_name(&self) -> String { let mut output = String::new(); - format_call_path_segments(self.call_path(), &mut output).unwrap(); + format_qualified_name_segments(self.call_path(), &mut output).unwrap(); output } } @@ -569,12 +569,12 @@ pub trait Imported<'a> { impl<'a> Imported<'a> for Import<'a> { /// For example, given `import foo`, returns `["foo"]`. fn call_path(&self) -> &[&'a str] { - self.call_path.as_ref() + self.qualified_name.as_ref() } /// For example, given `import foo`, returns `["foo"]`. fn module_name(&self) -> &[&'a str] { - &self.call_path[..1] + &self.qualified_name[..1] } /// For example, given `import foo`, returns `"foo"`. @@ -586,12 +586,12 @@ impl<'a> Imported<'a> for Import<'a> { impl<'a> Imported<'a> for SubmoduleImport<'a> { /// For example, given `import foo.bar`, returns `["foo", "bar"]`. fn call_path(&self) -> &[&'a str] { - self.call_path.as_ref() + self.qualified_name.as_ref() } /// For example, given `import foo.bar`, returns `["foo"]`. fn module_name(&self) -> &[&'a str] { - &self.call_path[..1] + &self.qualified_name[..1] } /// For example, given `import foo.bar`, returns `"foo.bar"`. @@ -603,17 +603,17 @@ impl<'a> Imported<'a> for SubmoduleImport<'a> { impl<'a> Imported<'a> for FromImport<'a> { /// For example, given `from foo import bar`, returns `["foo", "bar"]`. fn call_path(&self) -> &[&'a str] { - &self.call_path + &self.qualified_name } /// For example, given `from foo import bar`, returns `["foo"]`. fn module_name(&self) -> &[&'a str] { - &self.call_path[..self.call_path.len() - 1] + &self.qualified_name[..self.qualified_name.len() - 1] } /// For example, given `from foo import bar`, returns `"bar"`. fn member_name(&self) -> Cow<'a, str> { - Cow::Borrowed(self.call_path[self.call_path.len() - 1]) + Cow::Borrowed(self.qualified_name[self.qualified_name.len() - 1]) } } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 6608a4aa8520e..3556fb28a9b76 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -3,8 +3,8 @@ use std::path::Path; use bitflags::bitflags; use rustc_hash::FxHashMap; -use ruff_python_ast::call_path::{CallPath, CallPathBuilder}; use ruff_python_ast::helpers::from_relative_import; +use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder, UnqualifiedName}; use ruff_python_ast::{self as ast, Expr, Operator, Stmt}; use ruff_python_stdlib::path::is_python_stub_file; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -173,25 +173,31 @@ impl<'a> SemanticModel<'a> { pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool { self.seen_typing() && self - .resolve_call_path(expr) - .is_some_and(|call_path| self.match_typing_call_path(&call_path, target)) + .resolve_qualified_name(expr) + .is_some_and(|qualified_name| { + self.match_typing_qualified_name(&qualified_name, target) + }) } /// Return `true` if the call path is a reference to `typing.${target}`. - pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { + pub fn match_typing_qualified_name( + &self, + qualified_name: &QualifiedName, + target: &str, + ) -> bool { if matches!( - call_path.segments(), + qualified_name.segments(), ["typing" | "_typeshed" | "typing_extensions", member] if *member == target ) { return true; } if self.typing_modules.iter().any(|module| { - let module = CallPath::from_unqualified_name(module); - let mut builder = CallPathBuilder::from_path(module); + let module = QualifiedName::from_dotted_name(module); + let mut builder = QualifiedNameBuilder::from_qualified_name(module); builder.push(target); let target_path = builder.build(); - call_path == &target_path + qualified_name == &target_path }) { return true; } @@ -568,10 +574,10 @@ impl<'a> SemanticModel<'a> { /// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with /// `Class.method`. pub fn lookup_attribute(&self, value: &Expr) -> Option { - let call_path = CallPath::from_expr(value)?; + let unqualified_name = UnqualifiedName::from_expr(value)?; // Find the symbol in the current scope. - let (symbol, attribute) = call_path.segments().split_first()?; + let (symbol, attribute) = unqualified_name.segments().split_first()?; let mut binding_id = self.lookup_symbol(symbol)?; // Recursively resolve class attributes, e.g., `foo.bar.baz` in. @@ -659,10 +665,10 @@ impl<'a> SemanticModel<'a> { /// ``` /// /// ...then `resolve_call_path(${python_version})` will resolve to `sys.version_info`. - pub fn resolve_call_path<'name, 'expr: 'name>( + pub fn resolve_qualified_name<'name, 'expr: 'name>( &self, value: &'expr Expr, - ) -> Option> + ) -> Option> where 'a: 'name, { @@ -683,53 +689,61 @@ impl<'a> SemanticModel<'a> { .map(|id| self.binding(id))?; match &binding.kind { - BindingKind::Import(Import { call_path }) => { - let value_path = CallPath::from_expr(value)?; - let (_, tail) = value_path.segments().split_first()?; - let resolved: CallPath = call_path.iter().chain(tail.iter()).copied().collect(); + BindingKind::Import(Import { qualified_name }) => { + let unqualified_name = UnqualifiedName::from_expr(value)?; + let (_, tail) = unqualified_name.segments().split_first()?; + let resolved: QualifiedName = + qualified_name.iter().chain(tail.iter()).copied().collect(); Some(resolved) } - BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => { - let value_path = CallPath::from_expr(value)?; - let (_, tail) = value_path.segments().split_first()?; - let mut builder = CallPathBuilder::with_capacity(1 + tail.len()); - builder.extend(call_path.iter().copied().take(1)); - builder.extend(tail.iter().copied()); - Some(builder.build()) + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { + let value_name = UnqualifiedName::from_expr(value)?; + let (_, tail) = value_name.segments().split_first()?; + + Some( + qualified_name + .iter() + .take(1) + .chain(tail.iter()) + .copied() + .collect(), + ) } - BindingKind::FromImport(FromImport { call_path }) => { - let value_path = CallPath::from_expr(value)?; - let (_, tail) = value_path.segments().split_first()?; - - let resolved: CallPath = - if call_path.first().map_or(false, |segment| *segment == ".") { - from_relative_import(self.module_path?, call_path, tail)? - } else { - call_path.iter().chain(tail.iter()).copied().collect() - }; + BindingKind::FromImport(FromImport { qualified_name }) => { + let value_name = UnqualifiedName::from_expr(value)?; + let (_, tail) = value_name.segments().split_first()?; + + let resolved: QualifiedName = if qualified_name + .first() + .map_or(false, |segment| *segment == ".") + { + from_relative_import(self.module_path?, qualified_name, tail)? + } else { + qualified_name.iter().chain(tail.iter()).copied().collect() + }; Some(resolved) } BindingKind::Builtin => { if value.is_name_expr() { // Ex) `dict` - Some(CallPath::from_slice(&["", head.id.as_str()])) + Some(QualifiedName::from_slice(&["", head.id.as_str()])) } else { // Ex) `dict.__dict__` - let value_path = CallPath::from_expr(value)?; + let value_name = UnqualifiedName::from_expr(value)?; Some( std::iter::once("") - .chain(value_path.segments().iter().copied()) + .chain(value_name.segments().iter().copied()) .collect(), ) } } BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) => { - let value_path = CallPath::from_expr(value)?; - let resolved: CallPath = self + let value_name = UnqualifiedName::from_expr(value)?; + let resolved: QualifiedName = self .module_path? .iter() .map(String::as_str) - .chain(value_path.segments().iter().copied()) + .chain(value_name.segments().iter().copied()) .collect(); Some(resolved) } @@ -765,8 +779,8 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Import(Import { call_path }) => { - if call_path.as_ref() == module_path.as_slice() { + BindingKind::Import(Import { qualified_name }) => { + if qualified_name.as_ref() == module_path.as_slice() { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. if self @@ -787,8 +801,10 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImport(FromImport { call_path }) => { - if let Some((target_member, target_module)) = call_path.split_last() { + BindingKind::FromImport(FromImport { qualified_name }) => { + if let Some((target_member, target_module)) = + qualified_name.split_last() + { if target_module == module_path.as_slice() && target_member == &member { @@ -814,8 +830,8 @@ impl<'a> SemanticModel<'a> { // `import os.path ` -> `os.name` // Ex) Given `module="os.path"` and `object="join"`: // `import os.path ` -> `os.path.join` - BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => { - if call_path.starts_with(&module_path) { + BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }) => { + if qualified_name.starts_with(&module_path) { if let Some(source) = binding.source { // Verify that `os` isn't bound in an inner scope. if self diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 45b472ce707d6..a917dfa1ca2ca 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -2,9 +2,9 @@ /// can be used as `list[int]`). /// /// See: -pub fn is_standard_library_generic(call_path: &[&str]) -> bool { +pub fn is_standard_library_generic(qualified_name: &[&str]) -> bool { matches!( - call_path, + qualified_name, ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] | [ "collections" | "typing" | "typing_extensions", @@ -118,13 +118,16 @@ pub fn is_standard_library_generic(call_path: &[&str]) -> bool { /// See: /// /// [PEP 593]: https://peps.python.org/pep-0593/ -pub fn is_pep_593_generic_type(call_path: &[&str]) -> bool { - matches!(call_path, ["typing" | "typing_extensions", "Annotated"]) +pub fn is_pep_593_generic_type(qualified_name: &[&str]) -> bool { + matches!( + qualified_name, + ["typing" | "typing_extensions", "Annotated"] + ) } /// Returns `true` if a call path is `Literal`. -pub fn is_standard_library_literal(call_path: &[&str]) -> bool { - matches!(call_path, ["typing" | "typing_extensions", "Literal"]) +pub fn is_standard_library_literal(qualified_name: &[&str]) -> bool { + matches!(qualified_name, ["typing" | "typing_extensions", "Literal"]) } /// Returns `true` if a name matches that of a generic from the Python standard library (e.g. @@ -219,9 +222,9 @@ pub fn is_literal_member(member: &str) -> bool { /// Returns `true` if a call path represents that of an immutable, non-generic type from the Python /// standard library (e.g. `int` or `str`). -pub fn is_immutable_non_generic_type(call_path: &[&str]) -> bool { +pub fn is_immutable_non_generic_type(qualified_name: &[&str]) -> bool { matches!( - call_path, + qualified_name, ["collections", "abc", "Sized"] | ["typing", "LiteralString" | "Sized"] | [ @@ -241,9 +244,9 @@ pub fn is_immutable_non_generic_type(call_path: &[&str]) -> bool { /// Returns `true` if a call path represents that of an immutable, generic type from the Python /// standard library (e.g. `tuple`). -pub fn is_immutable_generic_type(call_path: &[&str]) -> bool { +pub fn is_immutable_generic_type(qualified_name: &[&str]) -> bool { matches!( - call_path, + qualified_name, ["", "tuple"] | [ "collections", @@ -279,9 +282,9 @@ pub fn is_immutable_generic_type(call_path: &[&str]) -> bool { /// Returns `true` if a call path represents a function from the Python standard library that /// returns a mutable value (e.g., `dict`). -pub fn is_mutable_return_type(call_path: &[&str]) -> bool { +pub fn is_mutable_return_type(qualified_name: &[&str]) -> bool { matches!( - call_path, + qualified_name, ["", "dict" | "list" | "set"] | [ "collections", @@ -292,9 +295,9 @@ pub fn is_mutable_return_type(call_path: &[&str]) -> bool { /// Returns `true` if a call path represents a function from the Python standard library that /// returns a immutable value (e.g., `bool`). -pub fn is_immutable_return_type(call_path: &[&str]) -> bool { +pub fn is_immutable_return_type(qualified_name: &[&str]) -> bool { matches!( - call_path, + qualified_name, ["datetime", "date" | "datetime" | "timedelta"] | ["decimal", "Decimal"] | ["fractions", "Fraction"]