mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00
Introduce a ruff_python_semantic
crate (#3865)
This commit is contained in:
parent
46bcb1f725
commit
d919adc13c
64 changed files with 267 additions and 225 deletions
69
crates/ruff_python_semantic/src/analyze/function_type.rs
Normal file
69
crates/ruff_python_semantic/src/analyze/function_type.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_python_ast::call_path::from_qualified_name;
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::scope::{Scope, ScopeKind};
|
||||
|
||||
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
|
||||
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum FunctionType {
|
||||
Function,
|
||||
Method,
|
||||
ClassMethod,
|
||||
StaticMethod,
|
||||
}
|
||||
|
||||
/// Classify a function based on its scope, name, and decorators.
|
||||
pub fn classify(
|
||||
ctx: &Context,
|
||||
scope: &Scope,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
classmethod_decorators: &[String],
|
||||
staticmethod_decorators: &[String],
|
||||
) -> FunctionType {
|
||||
let ScopeKind::Class(scope) = &scope.kind else {
|
||||
return FunctionType::Function;
|
||||
};
|
||||
if decorator_list.iter().any(|expr| {
|
||||
// The method is decorated with a static method decorator (like
|
||||
// `@staticmethod`).
|
||||
ctx.resolve_call_path(map_callable(expr))
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "staticmethod"]
|
||||
|| staticmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
}) {
|
||||
FunctionType::StaticMethod
|
||||
} else if CLASS_METHODS.contains(&name)
|
||||
// Special-case class method, like `__new__`.
|
||||
|| scope.bases.iter().any(|expr| {
|
||||
// The class itself extends a known metaclass, so all methods are class methods.
|
||||
ctx.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
|
||||
METACLASS_BASES
|
||||
.iter()
|
||||
.any(|(module, member)| call_path.as_slice() == [*module, *member])
|
||||
})
|
||||
})
|
||||
|| decorator_list.iter().any(|expr| {
|
||||
// The method is decorated with a class method decorator (like `@classmethod`).
|
||||
ctx.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "classmethod"] ||
|
||||
classmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
})
|
||||
{
|
||||
FunctionType::ClassMethod
|
||||
} else {
|
||||
// It's an instance method.
|
||||
FunctionType::Method
|
||||
}
|
||||
}
|
33
crates/ruff_python_semantic/src/analyze/logging.rs
Normal file
33
crates/ruff_python_semantic/src/analyze/logging.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use rustpython_parser::ast::{Expr, ExprKind};
|
||||
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
/// Return `true` if the given `Expr` is a potential logging call. Matches
|
||||
/// `logging.error`, `logger.error`, `self.logger.error`, etc., but not
|
||||
/// arbitrary `foo.error` calls.
|
||||
///
|
||||
/// It even matches direct `logging.error` calls even if the `logging` module
|
||||
/// is aliased. Example:
|
||||
/// ```python
|
||||
/// import logging as bar
|
||||
///
|
||||
/// # This is detected to be a logger candidate
|
||||
/// bar.error()
|
||||
/// ```
|
||||
pub fn is_logger_candidate(context: &Context, func: &Expr) -> bool {
|
||||
if let ExprKind::Attribute { value, .. } = &func.node {
|
||||
let Some(call_path) = context
|
||||
.resolve_call_path(value)
|
||||
.or_else(|| collect_call_path(value)) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(tail) = call_path.last() {
|
||||
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
4
crates/ruff_python_semantic/src/analyze/mod.rs
Normal file
4
crates/ruff_python_semantic/src/analyze/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod function_type;
|
||||
pub mod logging;
|
||||
pub mod typing;
|
||||
pub mod visibility;
|
70
crates/ruff_python_semantic/src/analyze/typing.rs
Normal file
70
crates/ruff_python_semantic/src/analyze/typing.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use rustpython_parser::ast::{Expr, ExprKind};
|
||||
|
||||
use ruff_python_ast::call_path::{from_unqualified_name, CallPath};
|
||||
use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS};
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Callable {
|
||||
Cast,
|
||||
NewType,
|
||||
TypeVar,
|
||||
NamedTuple,
|
||||
TypedDict,
|
||||
MypyExtension,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SubscriptKind {
|
||||
AnnotatedSubscript,
|
||||
PEP593AnnotatedSubscript,
|
||||
}
|
||||
|
||||
pub fn match_annotated_subscript<'a>(
|
||||
expr: &Expr,
|
||||
context: &Context,
|
||||
typing_modules: impl Iterator<Item = &'a str>,
|
||||
) -> Option<SubscriptKind> {
|
||||
if !matches!(
|
||||
expr.node,
|
||||
ExprKind::Name { .. } | ExprKind::Attribute { .. }
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
context.resolve_call_path(expr).and_then(|call_path| {
|
||||
if SUBSCRIPTS.contains(&call_path.as_slice()) {
|
||||
return Some(SubscriptKind::AnnotatedSubscript);
|
||||
}
|
||||
if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) {
|
||||
return Some(SubscriptKind::PEP593AnnotatedSubscript);
|
||||
}
|
||||
|
||||
for module in typing_modules {
|
||||
let module_call_path: CallPath = from_unqualified_name(module);
|
||||
if call_path.starts_with(&module_call_path) {
|
||||
for subscript in SUBSCRIPTS.iter() {
|
||||
if call_path.last() == subscript.last() {
|
||||
return Some(SubscriptKind::AnnotatedSubscript);
|
||||
}
|
||||
}
|
||||
for subscript in PEP_593_SUBSCRIPTS.iter() {
|
||||
if call_path.last() == subscript.last() {
|
||||
return Some(SubscriptKind::PEP593AnnotatedSubscript);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if `Expr` represents a reference to a typing object with a
|
||||
/// PEP 585 built-in.
|
||||
pub fn is_pep585_builtin(expr: &Expr, context: &Context) -> bool {
|
||||
context.resolve_call_path(expr).map_or(false, |call_path| {
|
||||
PEP_585_BUILTINS_ELIGIBLE.contains(&call_path.as_slice())
|
||||
})
|
||||
}
|
217
crates/ruff_python_semantic/src/analyze/visibility.rs
Normal file
217
crates/ruff_python_semantic/src/analyze/visibility.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
use ruff_python_ast::call_path::{collect_call_path, CallPath};
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Modifier {
|
||||
Module,
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Visibility {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VisibleScope {
|
||||
pub modifier: Modifier,
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a "static method".
|
||||
pub fn is_staticmethod(ctx: &Context, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list.iter().any(|expr| {
|
||||
ctx.resolve_call_path(map_callable(expr))
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "staticmethod"]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a "class method".
|
||||
pub fn is_classmethod(ctx: &Context, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list.iter().any(|expr| {
|
||||
ctx.resolve_call_path(map_callable(expr))
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "classmethod"]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is an `@overload`.
|
||||
pub fn is_overload(ctx: &Context, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list
|
||||
.iter()
|
||||
.any(|expr| ctx.match_typing_expr(map_callable(expr), "overload"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is an `@override` (PEP 698).
|
||||
pub fn is_override(ctx: &Context, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list
|
||||
.iter()
|
||||
.any(|expr| ctx.match_typing_expr(map_callable(expr), "override"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is an `@abstractmethod`.
|
||||
pub fn is_abstract(ctx: &Context, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list.iter().any(|expr| {
|
||||
ctx.resolve_call_path(map_callable(expr))
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["abc", "abstractmethod"]
|
||||
|| call_path.as_slice() == ["abc", "abstractproperty"]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if a function definition is a `@property`.
|
||||
/// `extra_properties` can be used to check additional non-standard
|
||||
/// `@property`-like decorators.
|
||||
pub fn is_property(ctx: &Context, decorator_list: &[Expr], extra_properties: &[CallPath]) -> bool {
|
||||
decorator_list.iter().any(|expr| {
|
||||
ctx.resolve_call_path(map_callable(expr))
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "property"]
|
||||
|| call_path.as_slice() == ["functools", "cached_property"]
|
||||
|| extra_properties
|
||||
.iter()
|
||||
.any(|extra_property| extra_property.as_slice() == call_path.as_slice())
|
||||
})
|
||||
})
|
||||
}
|
||||
/// Returns `true` if a function is a "magic method".
|
||||
pub fn is_magic(name: &str) -> bool {
|
||||
name.starts_with("__") && name.ends_with("__")
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is an `__init__`.
|
||||
pub fn is_init(name: &str) -> bool {
|
||||
name == "__init__"
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a `__new__`.
|
||||
pub fn is_new(name: &str) -> bool {
|
||||
name == "__new__"
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a `__call__`.
|
||||
pub fn is_call(name: &str) -> bool {
|
||||
name == "__call__"
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a test one.
|
||||
pub fn is_test(name: &str) -> bool {
|
||||
name == "runTest" || name.starts_with("test")
|
||||
}
|
||||
|
||||
/// Returns `true` if a module name indicates public visibility.
|
||||
fn is_public_module(module_name: &str) -> bool {
|
||||
!module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a module name indicates private visibility.
|
||||
fn is_private_module(module_name: &str) -> bool {
|
||||
!is_public_module(module_name)
|
||||
}
|
||||
|
||||
/// Return the stem of a module name (everything preceding the last dot).
|
||||
fn stem(path: &str) -> &str {
|
||||
if let Some(index) = path.rfind('.') {
|
||||
&path[..index]
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the `Visibility` of the Python file at `Path` based on its name.
|
||||
pub fn module_visibility(module_path: Option<&[String]>, path: &Path) -> Visibility {
|
||||
if let Some(module_path) = module_path {
|
||||
if module_path.iter().any(|m| is_private_module(m)) {
|
||||
return Visibility::Private;
|
||||
}
|
||||
} else {
|
||||
// When module_path is None, path is a script outside a package, so just
|
||||
// check to see if the module name itself is private.
|
||||
// Ex) `_foo.py` (but not `__init__.py`)
|
||||
let mut components = path.iter().rev();
|
||||
if let Some(filename) = components.next() {
|
||||
let module_name = filename.to_string_lossy();
|
||||
let module_name = stem(&module_name);
|
||||
if is_private_module(module_name) {
|
||||
return Visibility::Private;
|
||||
}
|
||||
}
|
||||
}
|
||||
Visibility::Public
|
||||
}
|
||||
|
||||
pub fn function_visibility(stmt: &Stmt) -> Visibility {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
|
||||
if name.starts_with('_') {
|
||||
Visibility::Private
|
||||
} else {
|
||||
Visibility::Public
|
||||
}
|
||||
}
|
||||
_ => panic!("Found non-FunctionDef in function_visibility"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method_visibility(stmt: &Stmt) -> Visibility {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef {
|
||||
name,
|
||||
decorator_list,
|
||||
..
|
||||
}
|
||||
| StmtKind::AsyncFunctionDef {
|
||||
name,
|
||||
decorator_list,
|
||||
..
|
||||
} => {
|
||||
// Is this a setter or deleter?
|
||||
if decorator_list.iter().any(|expr| {
|
||||
collect_call_path(expr).map_or(false, |call_path| {
|
||||
call_path.as_slice() == [name, "setter"]
|
||||
|| call_path.as_slice() == [name, "deleter"]
|
||||
})
|
||||
}) {
|
||||
return Visibility::Private;
|
||||
}
|
||||
|
||||
// Is the method non-private?
|
||||
if !name.starts_with('_') {
|
||||
return Visibility::Public;
|
||||
}
|
||||
|
||||
// Is this a magic method?
|
||||
if name.starts_with("__") && name.ends_with("__") {
|
||||
return Visibility::Public;
|
||||
}
|
||||
|
||||
Visibility::Private
|
||||
}
|
||||
_ => panic!("Found non-FunctionDef in method_visibility"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_visibility(stmt: &Stmt) -> Visibility {
|
||||
match &stmt.node {
|
||||
StmtKind::ClassDef { name, .. } => {
|
||||
if name.starts_with('_') {
|
||||
Visibility::Private
|
||||
} else {
|
||||
Visibility::Public
|
||||
}
|
||||
}
|
||||
_ => panic!("Found non-ClassDef in function_visibility"),
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue