CallPath newtype wrapper (#10201)

## Summary

This PR changes the `CallPath` type alias to a newtype wrapper. 

A newtype wrapper allows us to limit the API and to experiment with
alternative ways to implement matching on `CallPath`s.



## Test Plan

`cargo test`
This commit is contained in:
Micha Reiser 2024-03-03 16:54:24 +01:00 committed by GitHub
parent fb05d218c3
commit e725b6fdaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
165 changed files with 551 additions and 433 deletions

View file

@ -1,5 +1,4 @@
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::Decorator;
@ -38,7 +37,7 @@ pub fn classify(
semantic
.resolve_call_path(map_callable(expr))
.is_some_and( |call_path| {
matches!(call_path.as_slice(), ["", "type"] | ["abc", "ABCMeta"])
matches!(call_path.segments(), ["", "type"] | ["abc", "ABCMeta"])
})
})
|| decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators))
@ -63,11 +62,11 @@ fn is_static_method(
.resolve_call_path(decorator)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
) || staticmethod_decorators
.iter()
.any(|decorator| call_path == from_qualified_name(decorator))
.any(|decorator| call_path == CallPath::from_qualified_name(decorator))
})
{
return true;
@ -76,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 collect_call_path(decorator).is_some_and(|call_path| {
call_path.last().is_some_and(|tail| {
if CallPath::from_expr(decorator).is_some_and(|call_path| {
call_path.segments().last().is_some_and(|tail| {
staticmethod_decorators
.iter()
.any(|decorator| tail == decorator)
@ -103,11 +102,11 @@ fn is_class_method(
.resolve_call_path(decorator)
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
["", "classmethod"] | ["abc", "abstractclassmethod"]
) || classmethod_decorators
.iter()
.any(|decorator| call_path == from_qualified_name(decorator))
.any(|decorator| call_path == CallPath::from_qualified_name(decorator))
})
{
return true;
@ -116,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 collect_call_path(decorator).is_some_and(|call_path| {
call_path.last().is_some_and(|tail| {
if CallPath::from_expr(decorator).is_some_and(|call_path| {
call_path.segments().last().is_some_and(|tail| {
classmethod_decorators
.iter()
.any(|decorator| tail == decorator)

View file

@ -20,7 +20,7 @@ pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool {
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
[
"sys",
"path",
@ -50,7 +50,7 @@ pub fn is_os_environ_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
["os", "putenv" | "unsetenv"]
| [
"os",
@ -64,19 +64,19 @@ 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.as_slice(), ["os", "environ"]))
.is_some_and(|call_path| matches!(call_path.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.as_slice(), ["os", "environ"]))
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"]))
}),
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "environ"])),
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"])),
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => semantic
.resolve_call_path(map_subscript(target))
.is_some_and(|call_path| matches!(call_path.as_slice(), ["os", "environ"])),
.is_some_and(|call_path| matches!(call_path.segments(), ["os", "environ"])),
_ => false,
}
}
@ -96,5 +96,5 @@ pub fn is_matplotlib_activation(stmt: &Stmt, semantic: &SemanticModel) -> bool {
};
semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["matplotlib", "use"]))
.is_some_and(|call_path| matches!(call_path.segments(), ["matplotlib", "use"]))
}

View file

@ -1,4 +1,4 @@
use ruff_python_ast::call_path::{collect_call_path, from_qualified_name};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword};
@ -29,7 +29,7 @@ pub fn is_logger_candidate(
// logger object, the `logging` module itself, or `flask.current_app.logger`.
if let Some(call_path) = semantic.resolve_call_path(value) {
if matches!(
call_path.as_slice(),
call_path.segments(),
["logging"] | ["flask", "current_app", "logger"]
) {
return true;
@ -37,7 +37,7 @@ pub fn is_logger_candidate(
if logger_objects
.iter()
.any(|logger| from_qualified_name(logger) == call_path)
.any(|logger| CallPath::from_qualified_name(logger) == call_path)
{
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) = collect_call_path(value) {
if let Some(tail) = call_path.last() {
if let Some(call_path) = CallPath::from_expr(value) {
if let Some(tail) = call_path.segments().last() {
if tail.starts_with("log")
|| tail.ends_with("logger")
|| tail.ends_with("logging")
@ -79,7 +79,7 @@ pub fn exc_info<'a>(arguments: &'a Arguments, semantic: &SemanticModel) -> Optio
.value
.as_call_expr()
.and_then(|call| semantic.resolve_call_path(&call.func))
.is_some_and(|call_path| matches!(call_path.as_slice(), ["sys", "exc_info"]))
.is_some_and(|call_path| matches!(call_path.segments(), ["sys", "exc_info"]))
{
return Some(exc_info);
}

View file

@ -1,6 +1,6 @@
//! Analysis rules for the `typing` module.
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::{any_over_expr, is_const_false, map_subscript};
use ruff_python_ast::{self as ast, Expr, Int, Operator, ParameterWithDefault, Parameters, Stmt};
use ruff_python_stdlib::typing::{
@ -43,27 +43,27 @@ pub fn match_annotated_subscript<'a>(
extend_generics: &[String],
) -> Option<SubscriptKind> {
semantic.resolve_call_path(expr).and_then(|call_path| {
if is_standard_library_literal(call_path.as_slice()) {
if is_standard_library_literal(call_path.segments()) {
return Some(SubscriptKind::Literal);
}
if is_standard_library_generic(call_path.as_slice())
if is_standard_library_generic(call_path.segments())
|| extend_generics
.iter()
.map(|target| from_qualified_name(target))
.map(|target| CallPath::from_qualified_name(target))
.any(|target| call_path == target)
{
return Some(SubscriptKind::Generic);
}
if is_pep_593_generic_type(call_path.as_slice()) {
if is_pep_593_generic_type(call_path.segments()) {
return Some(SubscriptKind::PEP593Annotation);
}
for module in typing_modules {
let module_call_path: CallPath = from_unqualified_name(module);
let module_call_path: CallPath = CallPath::from_unqualified_name(module);
if call_path.starts_with(&module_call_path) {
if let Some(member) = call_path.last() {
if let Some(member) = call_path.segments().last() {
if is_literal_member(member) {
return Some(SubscriptKind::Literal);
}
@ -106,7 +106,7 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<Module
.then(|| semantic.resolve_call_path(expr))
.flatten()
.and_then(|call_path| {
let [module, member] = call_path.as_slice() else {
let [module, member] = call_path.segments() else {
return None;
};
as_pep_585_generic(module, member).map(|(module, member)| {
@ -122,7 +122,7 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<Module
/// Return whether a given expression uses a PEP 585 standard library generic.
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
let [module, name] = call_path.as_slice() else {
let [module, name] = call_path.segments() else {
return false;
};
has_pep_585_generic(module, name)
@ -218,8 +218,8 @@ pub fn is_immutable_annotation(
match expr {
Expr::Name(_) | Expr::Attribute(_) => {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
is_immutable_non_generic_type(call_path.as_slice())
|| is_immutable_generic_type(call_path.as_slice())
is_immutable_non_generic_type(call_path.segments())
|| is_immutable_generic_type(call_path.segments())
|| extend_immutable_calls
.iter()
.any(|target| call_path == *target)
@ -227,9 +227,9 @@ pub fn is_immutable_annotation(
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
semantic.resolve_call_path(value).is_some_and(|call_path| {
if is_immutable_generic_type(call_path.as_slice()) {
if is_immutable_generic_type(call_path.segments()) {
true
} else if matches!(call_path.as_slice(), ["typing", "Union"]) {
} else if matches!(call_path.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 +237,9 @@ pub fn is_immutable_annotation(
} else {
false
}
} else if matches!(call_path.as_slice(), ["typing", "Optional"]) {
} else if matches!(call_path.segments(), ["typing", "Optional"]) {
is_immutable_annotation(slice, semantic, extend_immutable_calls)
} else if is_pep_593_generic_type(call_path.as_slice()) {
} else if is_pep_593_generic_type(call_path.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)
@ -273,7 +273,7 @@ pub fn is_immutable_func(
extend_immutable_calls: &[CallPath],
) -> bool {
semantic.resolve_call_path(func).is_some_and(|call_path| {
is_immutable_return_type(call_path.as_slice())
is_immutable_return_type(call_path.segments())
|| extend_immutable_calls
.iter()
.any(|target| call_path == *target)
@ -285,7 +285,7 @@ pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(func)
.as_ref()
.map(CallPath::as_slice)
.map(CallPath::segments)
.is_some_and(is_mutable_return_type)
}
@ -337,7 +337,7 @@ pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> boo
any_over_expr(test, &|expr| {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
matches!(call_path.segments(), ["sys", "version_info" | "platform"])
})
})
}
@ -620,7 +620,7 @@ impl TypeChecker for IoBaseChecker {
return true;
}
matches!(
call_path.as_slice(),
call_path.segments(),
[
"io",
"IOBase"
@ -654,7 +654,7 @@ impl TypeChecker for IoBaseChecker {
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
return semantic.resolve_call_path(func).is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
[
"pathlib",
"Path" | "PurePath" | "PurePosixPath" | "PureWindowsPath"
@ -670,7 +670,7 @@ impl TypeChecker for IoBaseChecker {
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
["io", "open" | "open_code"] | ["os" | "", "open"]
)
})

View file

@ -2,7 +2,7 @@ use std::path::Path;
use ruff_python_ast::{self as ast, Decorator};
use ruff_python_ast::call_path::{collect_call_path, CallPath};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::helpers::map_callable;
use crate::model::SemanticModel;
@ -18,7 +18,7 @@ pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "staticmethod"]))
.is_some_and(|call_path| matches!(call_path.segments(), ["", "staticmethod"]))
})
}
@ -27,7 +27,7 @@ pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) ->
decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "classmethod"]))
.is_some_and(|call_path| matches!(call_path.segments(), ["", "classmethod"]))
})
}
@ -52,7 +52,7 @@ pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bo
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
[
"abc",
"abstractmethod"
@ -78,11 +78,11 @@ pub fn is_property(
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
call_path.segments(),
["", "property"] | ["functools", "cached_property"]
) || extra_properties
.iter()
.any(|extra_property| extra_property.as_slice() == call_path.as_slice())
.any(|extra_property| extra_property.segments() == call_path.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| {
collect_call_path(&decorator.expression).is_some_and(|call_path| {
call_path.as_slice() == [function.name.as_str(), "setter"]
|| call_path.as_slice() == [function.name.as_str(), "deleter"]
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"]
})
}) {
return Visibility::Private;

View file

@ -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;
use ruff_python_ast::call_path::format_call_path_segments;
use ruff_python_ast::Stmt;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@ -560,7 +560,9 @@ pub trait Imported<'a> {
/// Returns the fully-qualified name of the imported symbol.
fn qualified_name(&self) -> String {
format_call_path(self.call_path())
let mut output = String::new();
format_call_path_segments(self.call_path(), &mut output).unwrap();
output
}
}
@ -601,7 +603,7 @@ 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) -> &[&str] {
self.call_path.as_ref()
&self.call_path
}
/// For example, given `from foo import bar`, returns `["foo"]`.

View file

@ -2,9 +2,8 @@ use std::path::Path;
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use smallvec::smallvec;
use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath};
use ruff_python_ast::call_path::{CallPath, CallPathBuilder};
use ruff_python_ast::helpers::from_relative_import;
use ruff_python_ast::{self as ast, Expr, Operator, Stmt};
use ruff_python_stdlib::path::is_python_stub_file;
@ -181,16 +180,18 @@ impl<'a> SemanticModel<'a> {
/// 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 {
if matches!(
call_path.as_slice(),
call_path.segments(),
["typing" | "_typeshed" | "typing_extensions", member] if *member == target
) {
return true;
}
if self.typing_modules.iter().any(|module| {
let mut module: CallPath = from_unqualified_name(module);
module.push(target);
*call_path == module
let module = CallPath::from_unqualified_name(module);
let mut builder = CallPathBuilder::from_path(module);
builder.push(target);
let target_path = builder.build();
call_path == &target_path
}) {
return true;
}
@ -567,10 +568,10 @@ impl<'a> SemanticModel<'a> {
/// associated with `Class`, then the `BindingKind::FunctionDefinition` associated with
/// `Class.method`.
pub fn lookup_attribute(&'a self, value: &'a Expr) -> Option<BindingId> {
let call_path = collect_call_path(value)?;
let call_path = CallPath::from_expr(value)?;
// Find the symbol in the current scope.
let (symbol, attribute) = call_path.split_first()?;
let (symbol, attribute) = call_path.segments().split_first()?;
let mut binding_id = self.lookup_symbol(symbol)?;
// Recursively resolve class attributes, e.g., `foo.bar.baz` in.
@ -677,25 +678,22 @@ impl<'a> SemanticModel<'a> {
match &binding.kind {
BindingKind::Import(Import { call_path }) => {
let value_path = collect_call_path(value)?;
let (_, tail) = value_path.split_first()?;
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();
Some(resolved)
}
BindingKind::SubmoduleImport(SubmoduleImport { call_path }) => {
let value_path = collect_call_path(value)?;
let (_, tail) = value_path.split_first()?;
let resolved: CallPath = call_path
.iter()
.take(1)
.chain(tail.iter())
.copied()
.collect();
Some(resolved)
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::FromImport(FromImport { call_path }) => {
let value_path = collect_call_path(value)?;
let (_, tail) = value_path.split_first()?;
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 == ".") {
@ -708,24 +706,24 @@ impl<'a> SemanticModel<'a> {
BindingKind::Builtin => {
if value.is_name_expr() {
// Ex) `dict`
Some(smallvec!["", head.id.as_str()])
Some(CallPath::from_slice(&["", head.id.as_str()]))
} else {
// Ex) `dict.__dict__`
let value_path = collect_call_path(value)?;
let value_path = CallPath::from_expr(value)?;
Some(
std::iter::once("")
.chain(value_path.iter().copied())
.chain(value_path.segments().iter().copied())
.collect(),
)
}
}
BindingKind::ClassDefinition(_) | BindingKind::FunctionDefinition(_) => {
let value_path = collect_call_path(value)?;
let value_path = CallPath::from_expr(value)?;
let resolved: CallPath = self
.module_path?
.iter()
.map(String::as_str)
.chain(value_path)
.chain(value_path.segments().iter().copied())
.collect();
Some(resolved)
}