mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-29 06:44:16 +00:00
Create a rust_python_ast
crate (#3370)
This PR productionizes @MichaReiser's suggestion in https://github.com/charliermarsh/ruff/issues/1820#issuecomment-1440204423, by creating a separate crate for the `ast` module (`rust_python_ast`). This will enable us to further split up the `ruff` crate, as we'll be able to create (e.g.) separate sub-linter crates that have access to these common AST utilities. This was mostly a straightforward copy (with adjustments to module imports), as the few dependencies that _did_ require modifications were handled in #3366, #3367, and #3368.
This commit is contained in:
parent
a5d302fcbf
commit
bad6bdda1f
405 changed files with 1336 additions and 988 deletions
110
crates/ruff_python_ast/src/branch_detection.rs
Normal file
110
crates/ruff_python_ast/src/branch_detection.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::ExcepthandlerKind::ExceptHandler;
|
||||
use rustpython_parser::ast::{Stmt, StmtKind};
|
||||
|
||||
use crate::types::RefEquality;
|
||||
|
||||
/// Return the common ancestor of `left` and `right` below `stop`, or `None`.
|
||||
fn common_ancestor<'a>(
|
||||
left: &'a RefEquality<'a, Stmt>,
|
||||
right: &'a RefEquality<'a, Stmt>,
|
||||
stop: Option<&'a RefEquality<'a, Stmt>>,
|
||||
depths: &'a FxHashMap<RefEquality<'a, Stmt>, usize>,
|
||||
child_to_parent: &'a FxHashMap<RefEquality<'a, Stmt>, RefEquality<'a, Stmt>>,
|
||||
) -> Option<&'a RefEquality<'a, Stmt>> {
|
||||
if let Some(stop) = stop {
|
||||
if left == stop || right == stop {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if left == right {
|
||||
return Some(left);
|
||||
}
|
||||
|
||||
let left_depth = depths.get(left)?;
|
||||
let right_depth = depths.get(right)?;
|
||||
match left_depth.cmp(right_depth) {
|
||||
Ordering::Less => common_ancestor(
|
||||
left,
|
||||
child_to_parent.get(right)?,
|
||||
stop,
|
||||
depths,
|
||||
child_to_parent,
|
||||
),
|
||||
Ordering::Equal => common_ancestor(
|
||||
child_to_parent.get(left)?,
|
||||
child_to_parent.get(right)?,
|
||||
stop,
|
||||
depths,
|
||||
child_to_parent,
|
||||
),
|
||||
Ordering::Greater => common_ancestor(
|
||||
child_to_parent.get(left)?,
|
||||
right,
|
||||
stop,
|
||||
depths,
|
||||
child_to_parent,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the alternative branches for a given node.
|
||||
fn alternatives<'a>(stmt: &'a RefEquality<'a, Stmt>) -> Vec<Vec<RefEquality<'a, Stmt>>> {
|
||||
match &stmt.node {
|
||||
StmtKind::If { body, .. } => vec![body.iter().map(RefEquality).collect()],
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
..
|
||||
}
|
||||
| StmtKind::TryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
..
|
||||
} => vec![body.iter().chain(orelse.iter()).map(RefEquality).collect()]
|
||||
.into_iter()
|
||||
.chain(handlers.iter().map(|handler| {
|
||||
let ExceptHandler { body, .. } = &handler.node;
|
||||
body.iter().map(RefEquality).collect()
|
||||
}))
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if `stmt` is a descendent of any of the nodes in `ancestors`.
|
||||
fn descendant_of<'a>(
|
||||
stmt: &RefEquality<'a, Stmt>,
|
||||
ancestors: &[RefEquality<'a, Stmt>],
|
||||
stop: &RefEquality<'a, Stmt>,
|
||||
depths: &FxHashMap<RefEquality<'a, Stmt>, usize>,
|
||||
child_to_parent: &FxHashMap<RefEquality<'a, Stmt>, RefEquality<'a, Stmt>>,
|
||||
) -> bool {
|
||||
ancestors.iter().any(|ancestor| {
|
||||
common_ancestor(stmt, ancestor, Some(stop), depths, child_to_parent).is_some()
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if `left` and `right` are on different branches of an `if` or
|
||||
/// `try` statement.
|
||||
pub fn different_forks<'a>(
|
||||
left: &RefEquality<'a, Stmt>,
|
||||
right: &RefEquality<'a, Stmt>,
|
||||
depths: &FxHashMap<RefEquality<'a, Stmt>, usize>,
|
||||
child_to_parent: &FxHashMap<RefEquality<'a, Stmt>, RefEquality<'a, Stmt>>,
|
||||
) -> bool {
|
||||
if let Some(ancestor) = common_ancestor(left, right, None, depths, child_to_parent) {
|
||||
for items in alternatives(ancestor) {
|
||||
let l = descendant_of(left, &items, ancestor, depths, child_to_parent);
|
||||
let r = descendant_of(right, &items, ancestor, depths, child_to_parent);
|
||||
if l ^ r {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
16
crates/ruff_python_ast/src/cast.rs
Normal file
16
crates/ruff_python_ast/src/cast.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
pub fn name(stmt: &Stmt) -> &str {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => name,
|
||||
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { decorator_list, .. }
|
||||
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list,
|
||||
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
|
||||
}
|
||||
}
|
989
crates/ruff_python_ast/src/comparable.rs
Normal file
989
crates/ruff_python_ast/src/comparable.rs
Normal file
|
@ -0,0 +1,989 @@
|
|||
//! An equivalent object hierarchy to the [`Expr`] hierarchy, but with the
|
||||
//! ability to compare expressions for equality (via [`Eq`] and [`Hash`]).
|
||||
|
||||
use rustpython_parser::ast::{
|
||||
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
|
||||
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern,
|
||||
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
|
||||
};
|
||||
|
||||
use num_bigint::BigInt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableExprContext {
|
||||
Load,
|
||||
Store,
|
||||
Del,
|
||||
}
|
||||
|
||||
impl From<&ExprContext> for ComparableExprContext {
|
||||
fn from(ctx: &ExprContext) -> Self {
|
||||
match ctx {
|
||||
ExprContext::Load => Self::Load,
|
||||
ExprContext::Store => Self::Store,
|
||||
ExprContext::Del => Self::Del,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableBoolop {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
impl From<&Boolop> for ComparableBoolop {
|
||||
fn from(op: &Boolop) -> Self {
|
||||
match op {
|
||||
Boolop::And => Self::And,
|
||||
Boolop::Or => Self::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableOperator {
|
||||
Add,
|
||||
Sub,
|
||||
Mult,
|
||||
MatMult,
|
||||
Div,
|
||||
Mod,
|
||||
Pow,
|
||||
LShift,
|
||||
RShift,
|
||||
BitOr,
|
||||
BitXor,
|
||||
BitAnd,
|
||||
FloorDiv,
|
||||
}
|
||||
|
||||
impl From<&Operator> for ComparableOperator {
|
||||
fn from(op: &Operator) -> Self {
|
||||
match op {
|
||||
Operator::Add => Self::Add,
|
||||
Operator::Sub => Self::Sub,
|
||||
Operator::Mult => Self::Mult,
|
||||
Operator::MatMult => Self::MatMult,
|
||||
Operator::Div => Self::Div,
|
||||
Operator::Mod => Self::Mod,
|
||||
Operator::Pow => Self::Pow,
|
||||
Operator::LShift => Self::LShift,
|
||||
Operator::RShift => Self::RShift,
|
||||
Operator::BitOr => Self::BitOr,
|
||||
Operator::BitXor => Self::BitXor,
|
||||
Operator::BitAnd => Self::BitAnd,
|
||||
Operator::FloorDiv => Self::FloorDiv,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableUnaryop {
|
||||
Invert,
|
||||
Not,
|
||||
UAdd,
|
||||
USub,
|
||||
}
|
||||
|
||||
impl From<&Unaryop> for ComparableUnaryop {
|
||||
fn from(op: &Unaryop) -> Self {
|
||||
match op {
|
||||
Unaryop::Invert => Self::Invert,
|
||||
Unaryop::Not => Self::Not,
|
||||
Unaryop::UAdd => Self::UAdd,
|
||||
Unaryop::USub => Self::USub,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableCmpop {
|
||||
Eq,
|
||||
NotEq,
|
||||
Lt,
|
||||
LtE,
|
||||
Gt,
|
||||
GtE,
|
||||
Is,
|
||||
IsNot,
|
||||
In,
|
||||
NotIn,
|
||||
}
|
||||
|
||||
impl From<&Cmpop> for ComparableCmpop {
|
||||
fn from(op: &Cmpop) -> Self {
|
||||
match op {
|
||||
Cmpop::Eq => Self::Eq,
|
||||
Cmpop::NotEq => Self::NotEq,
|
||||
Cmpop::Lt => Self::Lt,
|
||||
Cmpop::LtE => Self::LtE,
|
||||
Cmpop::Gt => Self::Gt,
|
||||
Cmpop::GtE => Self::GtE,
|
||||
Cmpop::Is => Self::Is,
|
||||
Cmpop::IsNot => Self::IsNot,
|
||||
Cmpop::In => Self::In,
|
||||
Cmpop::NotIn => Self::NotIn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableAlias<'a> {
|
||||
pub name: &'a str,
|
||||
pub asname: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Alias> for ComparableAlias<'a> {
|
||||
fn from(alias: &'a Alias) -> Self {
|
||||
Self {
|
||||
name: &alias.node.name,
|
||||
asname: alias.node.asname.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableWithitem<'a> {
|
||||
pub context_expr: ComparableExpr<'a>,
|
||||
pub optional_vars: Option<ComparableExpr<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Withitem> for ComparableWithitem<'a> {
|
||||
fn from(withitem: &'a Withitem) -> Self {
|
||||
Self {
|
||||
context_expr: (&withitem.context_expr).into(),
|
||||
optional_vars: withitem.optional_vars.as_ref().map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparablePattern<'a> {
|
||||
MatchValue {
|
||||
value: ComparableExpr<'a>,
|
||||
},
|
||||
MatchSingleton {
|
||||
value: ComparableConstant<'a>,
|
||||
},
|
||||
MatchSequence {
|
||||
patterns: Vec<ComparablePattern<'a>>,
|
||||
},
|
||||
MatchMapping {
|
||||
keys: Vec<ComparableExpr<'a>>,
|
||||
patterns: Vec<ComparablePattern<'a>>,
|
||||
rest: Option<&'a str>,
|
||||
},
|
||||
MatchClass {
|
||||
cls: ComparableExpr<'a>,
|
||||
patterns: Vec<ComparablePattern<'a>>,
|
||||
kwd_attrs: Vec<&'a str>,
|
||||
kwd_patterns: Vec<ComparablePattern<'a>>,
|
||||
},
|
||||
MatchStar {
|
||||
name: Option<&'a str>,
|
||||
},
|
||||
MatchAs {
|
||||
pattern: Option<Box<ComparablePattern<'a>>>,
|
||||
name: Option<&'a str>,
|
||||
},
|
||||
MatchOr {
|
||||
patterns: Vec<ComparablePattern<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Pattern> for ComparablePattern<'a> {
|
||||
fn from(pattern: &'a Pattern) -> Self {
|
||||
match &pattern.node {
|
||||
PatternKind::MatchValue { value } => Self::MatchValue {
|
||||
value: value.into(),
|
||||
},
|
||||
PatternKind::MatchSingleton { value } => Self::MatchSingleton {
|
||||
value: value.into(),
|
||||
},
|
||||
PatternKind::MatchSequence { patterns } => Self::MatchSequence {
|
||||
patterns: patterns.iter().map(Into::into).collect(),
|
||||
},
|
||||
PatternKind::MatchMapping {
|
||||
keys,
|
||||
patterns,
|
||||
rest,
|
||||
} => Self::MatchMapping {
|
||||
keys: keys.iter().map(Into::into).collect(),
|
||||
patterns: patterns.iter().map(Into::into).collect(),
|
||||
rest: rest.as_deref(),
|
||||
},
|
||||
PatternKind::MatchClass {
|
||||
cls,
|
||||
patterns,
|
||||
kwd_attrs,
|
||||
kwd_patterns,
|
||||
} => Self::MatchClass {
|
||||
cls: cls.into(),
|
||||
patterns: patterns.iter().map(Into::into).collect(),
|
||||
kwd_attrs: kwd_attrs.iter().map(String::as_str).collect(),
|
||||
kwd_patterns: kwd_patterns.iter().map(Into::into).collect(),
|
||||
},
|
||||
PatternKind::MatchStar { name } => Self::MatchStar {
|
||||
name: name.as_deref(),
|
||||
},
|
||||
PatternKind::MatchAs { pattern, name } => Self::MatchAs {
|
||||
pattern: pattern.as_ref().map(Into::into),
|
||||
name: name.as_deref(),
|
||||
},
|
||||
PatternKind::MatchOr { patterns } => Self::MatchOr {
|
||||
patterns: patterns.iter().map(Into::into).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Pattern>> for Box<ComparablePattern<'a>> {
|
||||
fn from(pattern: &'a Box<Pattern>) -> Self {
|
||||
Box::new((&**pattern).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableMatchCase<'a> {
|
||||
pub pattern: ComparablePattern<'a>,
|
||||
pub guard: Option<ComparableExpr<'a>>,
|
||||
pub body: Vec<ComparableStmt<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a MatchCase> for ComparableMatchCase<'a> {
|
||||
fn from(match_case: &'a MatchCase) -> Self {
|
||||
Self {
|
||||
pattern: (&match_case.pattern).into(),
|
||||
guard: match_case.guard.as_ref().map(Into::into),
|
||||
body: match_case.body.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableConstant<'a> {
|
||||
None,
|
||||
Bool(&'a bool),
|
||||
Str(&'a str),
|
||||
Bytes(&'a [u8]),
|
||||
Int(&'a BigInt),
|
||||
Tuple(Vec<ComparableConstant<'a>>),
|
||||
Float(u64),
|
||||
Complex { real: u64, imag: u64 },
|
||||
Ellipsis,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Constant> for ComparableConstant<'a> {
|
||||
fn from(constant: &'a Constant) -> Self {
|
||||
match constant {
|
||||
Constant::None => Self::None,
|
||||
Constant::Bool(value) => Self::Bool(value),
|
||||
Constant::Str(value) => Self::Str(value),
|
||||
Constant::Bytes(value) => Self::Bytes(value),
|
||||
Constant::Int(value) => Self::Int(value),
|
||||
Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()),
|
||||
Constant::Float(value) => Self::Float(value.to_bits()),
|
||||
Constant::Complex { real, imag } => Self::Complex {
|
||||
real: real.to_bits(),
|
||||
imag: imag.to_bits(),
|
||||
},
|
||||
Constant::Ellipsis => Self::Ellipsis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableArguments<'a> {
|
||||
pub posonlyargs: Vec<ComparableArg<'a>>,
|
||||
pub args: Vec<ComparableArg<'a>>,
|
||||
pub vararg: Option<ComparableArg<'a>>,
|
||||
pub kwonlyargs: Vec<ComparableArg<'a>>,
|
||||
pub kw_defaults: Vec<ComparableExpr<'a>>,
|
||||
pub kwarg: Option<ComparableArg<'a>>,
|
||||
pub defaults: Vec<ComparableExpr<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Arguments> for ComparableArguments<'a> {
|
||||
fn from(arguments: &'a Arguments) -> Self {
|
||||
Self {
|
||||
posonlyargs: arguments.posonlyargs.iter().map(Into::into).collect(),
|
||||
args: arguments.args.iter().map(Into::into).collect(),
|
||||
vararg: arguments.vararg.as_ref().map(Into::into),
|
||||
kwonlyargs: arguments.kwonlyargs.iter().map(Into::into).collect(),
|
||||
kw_defaults: arguments.kw_defaults.iter().map(Into::into).collect(),
|
||||
kwarg: arguments.vararg.as_ref().map(Into::into),
|
||||
defaults: arguments.defaults.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Arguments>> for ComparableArguments<'a> {
|
||||
fn from(arguments: &'a Box<Arguments>) -> Self {
|
||||
(&**arguments).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Arg>> for ComparableArg<'a> {
|
||||
fn from(arg: &'a Box<Arg>) -> Self {
|
||||
(&**arg).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableArg<'a> {
|
||||
pub arg: &'a str,
|
||||
pub annotation: Option<Box<ComparableExpr<'a>>>,
|
||||
pub type_comment: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Arg> for ComparableArg<'a> {
|
||||
fn from(arg: &'a Arg) -> Self {
|
||||
Self {
|
||||
arg: &arg.node.arg,
|
||||
annotation: arg.node.annotation.as_ref().map(Into::into),
|
||||
type_comment: arg.node.type_comment.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableKeyword<'a> {
|
||||
pub arg: Option<&'a str>,
|
||||
pub value: ComparableExpr<'a>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Keyword> for ComparableKeyword<'a> {
|
||||
fn from(keyword: &'a Keyword) -> Self {
|
||||
Self {
|
||||
arg: keyword.node.arg.as_deref(),
|
||||
value: (&keyword.node.value).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableComprehension<'a> {
|
||||
pub target: ComparableExpr<'a>,
|
||||
pub iter: ComparableExpr<'a>,
|
||||
pub ifs: Vec<ComparableExpr<'a>>,
|
||||
pub is_async: &'a usize,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> {
|
||||
fn from(comprehension: &'a Comprehension) -> Self {
|
||||
Self {
|
||||
target: (&comprehension.target).into(),
|
||||
iter: (&comprehension.iter).into(),
|
||||
ifs: comprehension.ifs.iter().map(Into::into).collect(),
|
||||
is_async: &comprehension.is_async,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableExcepthandler<'a> {
|
||||
ExceptHandler {
|
||||
type_: Option<ComparableExpr<'a>>,
|
||||
name: Option<&'a str>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Excepthandler> for ComparableExcepthandler<'a> {
|
||||
fn from(excepthandler: &'a Excepthandler) -> Self {
|
||||
let ExcepthandlerKind::ExceptHandler { type_, name, body } = &excepthandler.node;
|
||||
Self::ExceptHandler {
|
||||
type_: type_.as_ref().map(Into::into),
|
||||
name: name.as_deref(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableExpr<'a> {
|
||||
BoolOp {
|
||||
op: ComparableBoolop,
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
NamedExpr {
|
||||
target: Box<ComparableExpr<'a>>,
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
BinOp {
|
||||
left: Box<ComparableExpr<'a>>,
|
||||
op: ComparableOperator,
|
||||
right: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
UnaryOp {
|
||||
op: ComparableUnaryop,
|
||||
operand: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Lambda {
|
||||
args: ComparableArguments<'a>,
|
||||
body: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
IfExp {
|
||||
test: Box<ComparableExpr<'a>>,
|
||||
body: Box<ComparableExpr<'a>>,
|
||||
orelse: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Dict {
|
||||
keys: Vec<Option<ComparableExpr<'a>>>,
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Set {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
ListComp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
SetComp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
DictComp {
|
||||
key: Box<ComparableExpr<'a>>,
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
GeneratorExp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
Await {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Yield {
|
||||
value: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
YieldFrom {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Compare {
|
||||
left: Box<ComparableExpr<'a>>,
|
||||
ops: Vec<ComparableCmpop>,
|
||||
comparators: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Call {
|
||||
func: Box<ComparableExpr<'a>>,
|
||||
args: Vec<ComparableExpr<'a>>,
|
||||
keywords: Vec<ComparableKeyword<'a>>,
|
||||
},
|
||||
FormattedValue {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
conversion: &'a usize,
|
||||
format_spec: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
JoinedStr {
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Constant {
|
||||
value: ComparableConstant<'a>,
|
||||
kind: Option<&'a str>,
|
||||
},
|
||||
Attribute {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
attr: &'a str,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Subscript {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
slice: Box<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Starred {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Name {
|
||||
id: &'a str,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
List {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Tuple {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Slice {
|
||||
lower: Option<Box<ComparableExpr<'a>>>,
|
||||
upper: Option<Box<ComparableExpr<'a>>>,
|
||||
step: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Expr>> for Box<ComparableExpr<'a>> {
|
||||
fn from(expr: &'a Box<Expr>) -> Self {
|
||||
Box::new((&**expr).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Expr>> for ComparableExpr<'a> {
|
||||
fn from(expr: &'a Box<Expr>) -> Self {
|
||||
(&**expr).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for ComparableExpr<'a> {
|
||||
fn from(expr: &'a Expr) -> Self {
|
||||
match &expr.node {
|
||||
ExprKind::BoolOp { op, values } => Self::BoolOp {
|
||||
op: op.into(),
|
||||
values: values.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::NamedExpr { target, value } => Self::NamedExpr {
|
||||
target: target.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::BinOp { left, op, right } => Self::BinOp {
|
||||
left: left.into(),
|
||||
op: op.into(),
|
||||
right: right.into(),
|
||||
},
|
||||
ExprKind::UnaryOp { op, operand } => Self::UnaryOp {
|
||||
op: op.into(),
|
||||
operand: operand.into(),
|
||||
},
|
||||
ExprKind::Lambda { args, body } => Self::Lambda {
|
||||
args: (&**args).into(),
|
||||
body: body.into(),
|
||||
},
|
||||
ExprKind::IfExp { test, body, orelse } => Self::IfExp {
|
||||
test: test.into(),
|
||||
body: body.into(),
|
||||
orelse: orelse.into(),
|
||||
},
|
||||
ExprKind::Dict { keys, values } => Self::Dict {
|
||||
keys: keys
|
||||
.iter()
|
||||
.map(|expr| expr.as_ref().map(Into::into))
|
||||
.collect(),
|
||||
values: values.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::Set { elts } => Self::Set {
|
||||
elts: elts.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::ListComp { elt, generators } => Self::ListComp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::SetComp { elt, generators } => Self::SetComp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::DictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
} => Self::DictComp {
|
||||
key: key.into(),
|
||||
value: value.into(),
|
||||
generators: generators.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::GeneratorExp { elt, generators } => Self::GeneratorExp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::Await { value } => Self::Await {
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::Yield { value } => Self::Yield {
|
||||
value: value.as_ref().map(Into::into),
|
||||
},
|
||||
ExprKind::YieldFrom { value } => Self::YieldFrom {
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} => Self::Compare {
|
||||
left: left.into(),
|
||||
ops: ops.iter().map(Into::into).collect(),
|
||||
comparators: comparators.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => Self::Call {
|
||||
func: func.into(),
|
||||
args: args.iter().map(Into::into).collect(),
|
||||
keywords: keywords.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::FormattedValue {
|
||||
value,
|
||||
conversion,
|
||||
format_spec,
|
||||
} => Self::FormattedValue {
|
||||
value: value.into(),
|
||||
conversion,
|
||||
format_spec: format_spec.as_ref().map(Into::into),
|
||||
},
|
||||
ExprKind::JoinedStr { values } => Self::JoinedStr {
|
||||
values: values.iter().map(Into::into).collect(),
|
||||
},
|
||||
ExprKind::Constant { value, kind } => Self::Constant {
|
||||
value: value.into(),
|
||||
kind: kind.as_ref().map(String::as_str),
|
||||
},
|
||||
ExprKind::Attribute { value, attr, ctx } => Self::Attribute {
|
||||
value: value.into(),
|
||||
attr,
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Subscript { value, slice, ctx } => Self::Subscript {
|
||||
value: value.into(),
|
||||
slice: slice.into(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Starred { value, ctx } => Self::Starred {
|
||||
value: value.into(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Name { id, ctx } => Self::Name {
|
||||
id,
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::List { elts, ctx } => Self::List {
|
||||
elts: elts.iter().map(Into::into).collect(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Tuple { elts, ctx } => Self::Tuple {
|
||||
elts: elts.iter().map(Into::into).collect(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Slice { lower, upper, step } => Self::Slice {
|
||||
lower: lower.as_ref().map(Into::into),
|
||||
upper: upper.as_ref().map(Into::into),
|
||||
step: step.as_ref().map(Into::into),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableStmt<'a> {
|
||||
FunctionDef {
|
||||
name: &'a str,
|
||||
args: ComparableArguments<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
decorator_list: Vec<ComparableExpr<'a>>,
|
||||
returns: Option<ComparableExpr<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
AsyncFunctionDef {
|
||||
name: &'a str,
|
||||
args: ComparableArguments<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
decorator_list: Vec<ComparableExpr<'a>>,
|
||||
returns: Option<ComparableExpr<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
ClassDef {
|
||||
name: &'a str,
|
||||
bases: Vec<ComparableExpr<'a>>,
|
||||
keywords: Vec<ComparableKeyword<'a>>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
decorator_list: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Return {
|
||||
value: Option<ComparableExpr<'a>>,
|
||||
},
|
||||
Delete {
|
||||
targets: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Assign {
|
||||
targets: Vec<ComparableExpr<'a>>,
|
||||
value: ComparableExpr<'a>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
AugAssign {
|
||||
target: ComparableExpr<'a>,
|
||||
op: ComparableOperator,
|
||||
value: ComparableExpr<'a>,
|
||||
},
|
||||
AnnAssign {
|
||||
target: ComparableExpr<'a>,
|
||||
annotation: ComparableExpr<'a>,
|
||||
value: Option<ComparableExpr<'a>>,
|
||||
simple: usize,
|
||||
},
|
||||
For {
|
||||
target: ComparableExpr<'a>,
|
||||
iter: ComparableExpr<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
AsyncFor {
|
||||
target: ComparableExpr<'a>,
|
||||
iter: ComparableExpr<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
While {
|
||||
test: ComparableExpr<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
},
|
||||
If {
|
||||
test: ComparableExpr<'a>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
},
|
||||
With {
|
||||
items: Vec<ComparableWithitem<'a>>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
AsyncWith {
|
||||
items: Vec<ComparableWithitem<'a>>,
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
type_comment: Option<&'a str>,
|
||||
},
|
||||
Match {
|
||||
subject: ComparableExpr<'a>,
|
||||
cases: Vec<ComparableMatchCase<'a>>,
|
||||
},
|
||||
Raise {
|
||||
exc: Option<ComparableExpr<'a>>,
|
||||
cause: Option<ComparableExpr<'a>>,
|
||||
},
|
||||
Try {
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
handlers: Vec<ComparableExcepthandler<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
finalbody: Vec<ComparableStmt<'a>>,
|
||||
},
|
||||
TryStar {
|
||||
body: Vec<ComparableStmt<'a>>,
|
||||
handlers: Vec<ComparableExcepthandler<'a>>,
|
||||
orelse: Vec<ComparableStmt<'a>>,
|
||||
finalbody: Vec<ComparableStmt<'a>>,
|
||||
},
|
||||
Assert {
|
||||
test: ComparableExpr<'a>,
|
||||
msg: Option<ComparableExpr<'a>>,
|
||||
},
|
||||
Import {
|
||||
names: Vec<ComparableAlias<'a>>,
|
||||
},
|
||||
ImportFrom {
|
||||
module: Option<&'a str>,
|
||||
names: Vec<ComparableAlias<'a>>,
|
||||
level: Option<usize>,
|
||||
},
|
||||
Global {
|
||||
names: Vec<&'a str>,
|
||||
},
|
||||
Nonlocal {
|
||||
names: Vec<&'a str>,
|
||||
},
|
||||
Expr {
|
||||
value: ComparableExpr<'a>,
|
||||
},
|
||||
Pass,
|
||||
Break,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Stmt> for ComparableStmt<'a> {
|
||||
fn from(stmt: &'a Stmt) -> Self {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef {
|
||||
name,
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
type_comment,
|
||||
} => Self::FunctionDef {
|
||||
name,
|
||||
args: args.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
decorator_list: decorator_list.iter().map(Into::into).collect(),
|
||||
returns: returns.as_ref().map(Into::into),
|
||||
type_comment: type_comment.as_ref().map(std::string::String::as_str),
|
||||
},
|
||||
StmtKind::AsyncFunctionDef {
|
||||
name,
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
type_comment,
|
||||
} => Self::AsyncFunctionDef {
|
||||
name,
|
||||
args: args.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
decorator_list: decorator_list.iter().map(Into::into).collect(),
|
||||
returns: returns.as_ref().map(Into::into),
|
||||
type_comment: type_comment.as_ref().map(std::string::String::as_str),
|
||||
},
|
||||
StmtKind::ClassDef {
|
||||
name,
|
||||
bases,
|
||||
keywords,
|
||||
body,
|
||||
decorator_list,
|
||||
} => Self::ClassDef {
|
||||
name,
|
||||
bases: bases.iter().map(Into::into).collect(),
|
||||
keywords: keywords.iter().map(Into::into).collect(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
decorator_list: decorator_list.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::Return { value } => Self::Return {
|
||||
value: value.as_ref().map(Into::into),
|
||||
},
|
||||
StmtKind::Delete { targets } => Self::Delete {
|
||||
targets: targets.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::Assign {
|
||||
targets,
|
||||
value,
|
||||
type_comment,
|
||||
} => Self::Assign {
|
||||
targets: targets.iter().map(Into::into).collect(),
|
||||
value: value.into(),
|
||||
type_comment: type_comment.as_ref().map(std::string::String::as_str),
|
||||
},
|
||||
StmtKind::AugAssign { target, op, value } => Self::AugAssign {
|
||||
target: target.into(),
|
||||
op: op.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
StmtKind::AnnAssign {
|
||||
target,
|
||||
annotation,
|
||||
value,
|
||||
simple,
|
||||
} => Self::AnnAssign {
|
||||
target: target.into(),
|
||||
annotation: annotation.into(),
|
||||
value: value.as_ref().map(Into::into),
|
||||
simple: *simple,
|
||||
},
|
||||
StmtKind::For {
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
type_comment,
|
||||
} => Self::For {
|
||||
target: target.into(),
|
||||
iter: iter.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
type_comment: type_comment.as_ref().map(String::as_str),
|
||||
},
|
||||
StmtKind::AsyncFor {
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
type_comment,
|
||||
} => Self::AsyncFor {
|
||||
target: target.into(),
|
||||
iter: iter.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
type_comment: type_comment.as_ref().map(String::as_str),
|
||||
},
|
||||
StmtKind::While { test, body, orelse } => Self::While {
|
||||
test: test.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::If { test, body, orelse } => Self::If {
|
||||
test: test.into(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::With {
|
||||
items,
|
||||
body,
|
||||
type_comment,
|
||||
} => Self::With {
|
||||
items: items.iter().map(Into::into).collect(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
type_comment: type_comment.as_ref().map(String::as_str),
|
||||
},
|
||||
StmtKind::AsyncWith {
|
||||
items,
|
||||
body,
|
||||
type_comment,
|
||||
} => Self::AsyncWith {
|
||||
items: items.iter().map(Into::into).collect(),
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
type_comment: type_comment.as_ref().map(String::as_str),
|
||||
},
|
||||
StmtKind::Match { subject, cases } => Self::Match {
|
||||
subject: subject.into(),
|
||||
cases: cases.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::Raise { exc, cause } => Self::Raise {
|
||||
exc: exc.as_ref().map(Into::into),
|
||||
cause: cause.as_ref().map(Into::into),
|
||||
},
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => Self::Try {
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
handlers: handlers.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
finalbody: finalbody.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::TryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => Self::TryStar {
|
||||
body: body.iter().map(Into::into).collect(),
|
||||
handlers: handlers.iter().map(Into::into).collect(),
|
||||
orelse: orelse.iter().map(Into::into).collect(),
|
||||
finalbody: finalbody.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::Assert { test, msg } => Self::Assert {
|
||||
test: test.into(),
|
||||
msg: msg.as_ref().map(Into::into),
|
||||
},
|
||||
StmtKind::Import { names } => Self::Import {
|
||||
names: names.iter().map(Into::into).collect(),
|
||||
},
|
||||
StmtKind::ImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
} => Self::ImportFrom {
|
||||
module: module.as_ref().map(String::as_str),
|
||||
names: names.iter().map(Into::into).collect(),
|
||||
level: *level,
|
||||
},
|
||||
StmtKind::Global { names } => Self::Global {
|
||||
names: names.iter().map(String::as_str).collect(),
|
||||
},
|
||||
StmtKind::Nonlocal { names } => Self::Nonlocal {
|
||||
names: names.iter().map(String::as_str).collect(),
|
||||
},
|
||||
StmtKind::Expr { value } => Self::Expr {
|
||||
value: value.into(),
|
||||
},
|
||||
StmtKind::Pass => Self::Pass,
|
||||
StmtKind::Break => Self::Break,
|
||||
StmtKind::Continue => Self::Continue,
|
||||
}
|
||||
}
|
||||
}
|
292
crates/ruff_python_ast/src/context.rs
Normal file
292
crates/ruff_python_ast/src/context.rs
Normal file
|
@ -0,0 +1,292 @@
|
|||
use std::path::Path;
|
||||
|
||||
use nohash_hasher::IntMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Expr, Stmt};
|
||||
use smallvec::smallvec;
|
||||
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
use ruff_python_stdlib::typing::TYPING_EXTENSIONS;
|
||||
|
||||
use crate::helpers::{collect_call_path, from_relative_import, Exceptions};
|
||||
use crate::types::{Binding, BindingKind, CallPath, ExecutionContext, RefEquality, Scope};
|
||||
use crate::visibility::{module_visibility, Modifier, VisibleScope};
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Context<'a> {
|
||||
pub typing_modules: &'a [String],
|
||||
pub module_path: Option<Vec<String>>,
|
||||
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
|
||||
// at various points in time.
|
||||
pub parents: Vec<RefEquality<'a, Stmt>>,
|
||||
pub depths: FxHashMap<RefEquality<'a, Stmt>, usize>,
|
||||
pub child_to_parent: FxHashMap<RefEquality<'a, Stmt>, RefEquality<'a, Stmt>>,
|
||||
// A stack of all bindings created in any scope, at any point in execution.
|
||||
pub bindings: Vec<Binding<'a>>,
|
||||
// Map from binding index to indexes of bindings that redefine it in other scopes.
|
||||
pub redefinitions: IntMap<usize, Vec<usize>>,
|
||||
pub exprs: Vec<RefEquality<'a, Expr>>,
|
||||
pub scopes: Vec<Scope<'a>>,
|
||||
pub scope_stack: Vec<usize>,
|
||||
pub dead_scopes: Vec<(usize, Vec<usize>)>,
|
||||
// Body iteration; used to peek at siblings.
|
||||
pub body: &'a [Stmt],
|
||||
pub body_index: usize,
|
||||
// Internal, derivative state.
|
||||
pub visible_scope: VisibleScope,
|
||||
pub in_annotation: bool,
|
||||
pub in_type_definition: bool,
|
||||
pub in_deferred_string_type_definition: bool,
|
||||
pub in_deferred_type_definition: bool,
|
||||
pub in_exception_handler: bool,
|
||||
pub in_literal: bool,
|
||||
pub in_subscript: bool,
|
||||
pub in_type_checking_block: bool,
|
||||
pub seen_import_boundary: bool,
|
||||
pub futures_allowed: bool,
|
||||
pub annotations_future_enabled: bool,
|
||||
pub handled_exceptions: Vec<Exceptions>,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(
|
||||
typing_modules: &'a [String],
|
||||
path: &'a Path,
|
||||
module_path: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
typing_modules,
|
||||
module_path,
|
||||
parents: Vec::default(),
|
||||
depths: FxHashMap::default(),
|
||||
child_to_parent: FxHashMap::default(),
|
||||
bindings: Vec::default(),
|
||||
redefinitions: IntMap::default(),
|
||||
exprs: Vec::default(),
|
||||
scopes: Vec::default(),
|
||||
scope_stack: Vec::default(),
|
||||
dead_scopes: Vec::default(),
|
||||
body: &[],
|
||||
body_index: 0,
|
||||
visible_scope: VisibleScope {
|
||||
modifier: Modifier::Module,
|
||||
visibility: module_visibility(path),
|
||||
},
|
||||
in_annotation: false,
|
||||
in_type_definition: false,
|
||||
in_deferred_string_type_definition: false,
|
||||
in_deferred_type_definition: false,
|
||||
in_exception_handler: false,
|
||||
in_literal: false,
|
||||
in_subscript: false,
|
||||
in_type_checking_block: false,
|
||||
seen_import_boundary: false,
|
||||
futures_allowed: true,
|
||||
annotations_future_enabled: is_python_stub_file(path),
|
||||
handled_exceptions: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
|
||||
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
|
||||
self.resolve_call_path(expr).map_or(false, |call_path| {
|
||||
self.match_typing_call_path(&call_path, 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 {
|
||||
if call_path.as_slice() == ["typing", target] {
|
||||
return true;
|
||||
}
|
||||
|
||||
if TYPING_EXTENSIONS.contains(target) {
|
||||
if call_path.as_slice() == ["typing_extensions", target] {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.typing_modules.iter().any(|module| {
|
||||
let mut module: CallPath = module.split('.').collect();
|
||||
module.push(target);
|
||||
*call_path == module
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the current `Binding` for a given `name`.
|
||||
pub fn find_binding(&self, member: &str) -> Option<&Binding> {
|
||||
self.current_scopes()
|
||||
.find_map(|scope| scope.bindings.get(member))
|
||||
.map(|index| &self.bindings[*index])
|
||||
}
|
||||
|
||||
/// Return `true` if `member` is bound as a builtin.
|
||||
pub fn is_builtin(&self, member: &str) -> bool {
|
||||
self.find_binding(member)
|
||||
.map_or(false, |binding| binding.kind.is_builtin())
|
||||
}
|
||||
|
||||
/// Resolves the call path, e.g. if you have a file
|
||||
///
|
||||
/// ```python
|
||||
/// from sys import version_info as python_version
|
||||
/// print(python_version)
|
||||
/// ```
|
||||
///
|
||||
/// then `python_version` from the print statement will resolve to `sys.version_info`.
|
||||
pub fn resolve_call_path<'b>(&'a self, value: &'b Expr) -> Option<CallPath<'a>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let call_path = collect_call_path(value);
|
||||
let Some(head) = call_path.first() else {
|
||||
return None;
|
||||
};
|
||||
let Some(binding) = self.find_binding(head) else {
|
||||
return None;
|
||||
};
|
||||
match &binding.kind {
|
||||
BindingKind::Importation(.., name) | BindingKind::SubmoduleImportation(name, ..) => {
|
||||
if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(.., name) => {
|
||||
if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
}
|
||||
}
|
||||
BindingKind::Builtin => {
|
||||
let mut source_path: CallPath = smallvec![];
|
||||
source_path.push("");
|
||||
source_path.extend(call_path);
|
||||
Some(source_path)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_parent(&mut self, parent: &'a Stmt) {
|
||||
let num_existing = self.parents.len();
|
||||
self.parents.push(RefEquality(parent));
|
||||
self.depths
|
||||
.insert(self.parents[num_existing].clone(), num_existing);
|
||||
if num_existing > 0 {
|
||||
self.child_to_parent.insert(
|
||||
self.parents[num_existing].clone(),
|
||||
self.parents[num_existing - 1].clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_parent(&mut self) {
|
||||
self.parents.pop().expect("Attempted to pop without parent");
|
||||
}
|
||||
|
||||
pub fn push_expr(&mut self, expr: &'a Expr) {
|
||||
self.exprs.push(RefEquality(expr));
|
||||
}
|
||||
|
||||
pub fn pop_expr(&mut self) {
|
||||
self.exprs
|
||||
.pop()
|
||||
.expect("Attempted to pop without expression");
|
||||
}
|
||||
|
||||
pub fn push_scope(&mut self, scope: Scope<'a>) {
|
||||
self.scope_stack.push(self.scopes.len());
|
||||
self.scopes.push(scope);
|
||||
}
|
||||
|
||||
pub fn pop_scope(&mut self) {
|
||||
self.dead_scopes.push((
|
||||
self.scope_stack
|
||||
.pop()
|
||||
.expect("Attempted to pop without scope"),
|
||||
self.scope_stack.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Return the current `Stmt`.
|
||||
pub fn current_stmt(&self) -> &RefEquality<'a, Stmt> {
|
||||
self.parents.iter().rev().next().expect("No parent found")
|
||||
}
|
||||
|
||||
/// Return the parent `Stmt` of the current `Stmt`, if any.
|
||||
pub fn current_stmt_parent(&self) -> Option<&RefEquality<'a, Stmt>> {
|
||||
self.parents.iter().rev().nth(1)
|
||||
}
|
||||
|
||||
/// Return the parent `Expr` of the current `Expr`.
|
||||
pub fn current_expr_parent(&self) -> Option<&RefEquality<'a, Expr>> {
|
||||
self.exprs.iter().rev().nth(1)
|
||||
}
|
||||
|
||||
/// Return the grandparent `Expr` of the current `Expr`.
|
||||
pub fn current_expr_grandparent(&self) -> Option<&RefEquality<'a, Expr>> {
|
||||
self.exprs.iter().rev().nth(2)
|
||||
}
|
||||
|
||||
/// Return the `Stmt` that immediately follows the current `Stmt`, if any.
|
||||
pub fn current_sibling_stmt(&self) -> Option<&'a Stmt> {
|
||||
self.body.get(self.body_index + 1)
|
||||
}
|
||||
|
||||
pub fn current_scope(&self) -> &Scope {
|
||||
&self.scopes[*(self.scope_stack.last().expect("No current scope found"))]
|
||||
}
|
||||
|
||||
pub fn current_scope_parent(&self) -> Option<&Scope> {
|
||||
self.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.nth(1)
|
||||
.map(|index| &self.scopes[*index])
|
||||
}
|
||||
|
||||
pub fn current_scopes(&self) -> impl Iterator<Item = &Scope> {
|
||||
self.scope_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|index| &self.scopes[*index])
|
||||
}
|
||||
|
||||
pub const fn in_exception_handler(&self) -> bool {
|
||||
self.in_exception_handler
|
||||
}
|
||||
|
||||
pub const fn execution_context(&self) -> ExecutionContext {
|
||||
if self.in_type_checking_block
|
||||
|| self.in_annotation
|
||||
|| self.in_deferred_string_type_definition
|
||||
{
|
||||
ExecutionContext::Typing
|
||||
} else {
|
||||
ExecutionContext::Runtime
|
||||
}
|
||||
}
|
||||
}
|
66
crates/ruff_python_ast/src/function_type.rs
Normal file
66
crates/ruff_python_ast/src/function_type.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::helpers::{map_callable, to_call_path};
|
||||
use crate::types::{Scope, ScopeKind};
|
||||
|
||||
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
|
||||
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
|
||||
|
||||
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 == to_call_path(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 == to_call_path(decorator))
|
||||
})
|
||||
})
|
||||
{
|
||||
FunctionType::ClassMethod
|
||||
} else {
|
||||
// It's an instance method.
|
||||
FunctionType::Method
|
||||
}
|
||||
}
|
40
crates/ruff_python_ast/src/hashable.rs
Normal file
40
crates/ruff_python_ast/src/hashable.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::hash::Hash;
|
||||
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use crate::comparable::ComparableExpr;
|
||||
|
||||
/// Wrapper around `Expr` that implements `Hash` and `PartialEq`.
|
||||
pub struct HashableExpr<'a>(&'a Expr);
|
||||
|
||||
impl Hash for HashableExpr<'_> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let comparable = ComparableExpr::from(self.0);
|
||||
comparable.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for HashableExpr<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let comparable = ComparableExpr::from(self.0);
|
||||
comparable == ComparableExpr::from(other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for HashableExpr<'_> {}
|
||||
|
||||
impl<'a> From<&'a Expr> for HashableExpr<'a> {
|
||||
fn from(expr: &'a Expr) -> Self {
|
||||
Self(expr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> HashableExpr<'a> {
|
||||
pub const fn from_expr(expr: &'a Expr) -> Self {
|
||||
Self(expr)
|
||||
}
|
||||
|
||||
pub const fn as_expr(&self) -> &'a Expr {
|
||||
self.0
|
||||
}
|
||||
}
|
1468
crates/ruff_python_ast/src/helpers.rs
Normal file
1468
crates/ruff_python_ast/src/helpers.rs
Normal file
File diff suppressed because it is too large
Load diff
17
crates/ruff_python_ast/src/lib.rs
Normal file
17
crates/ruff_python_ast/src/lib.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
pub mod branch_detection;
|
||||
pub mod cast;
|
||||
pub mod comparable;
|
||||
pub mod context;
|
||||
pub mod function_type;
|
||||
pub mod hashable;
|
||||
pub mod helpers;
|
||||
pub mod logging;
|
||||
pub mod operations;
|
||||
pub mod relocate;
|
||||
pub mod source_code;
|
||||
pub mod strings;
|
||||
pub mod types;
|
||||
pub mod typing;
|
||||
pub mod visibility;
|
||||
pub mod visitor;
|
||||
pub mod whitespace;
|
24
crates/ruff_python_ast/src/logging.rs
Normal file
24
crates/ruff_python_ast/src/logging.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
pub enum LoggingLevel {
|
||||
Debug,
|
||||
Critical,
|
||||
Error,
|
||||
Exception,
|
||||
Info,
|
||||
Warn,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl LoggingLevel {
|
||||
pub fn from_attribute(level: &str) -> Option<Self> {
|
||||
match level {
|
||||
"debug" => Some(LoggingLevel::Debug),
|
||||
"critical" => Some(LoggingLevel::Critical),
|
||||
"error" => Some(LoggingLevel::Error),
|
||||
"exception" => Some(LoggingLevel::Exception),
|
||||
"info" => Some(LoggingLevel::Info),
|
||||
"warn" => Some(LoggingLevel::Warn),
|
||||
"warning" => Some(LoggingLevel::Warning),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
418
crates/ruff_python_ast/src/operations.rs
Normal file
418
crates/ruff_python_ast/src/operations.rs
Normal file
|
@ -0,0 +1,418 @@
|
|||
use bitflags::bitflags;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located, Stmt, StmtKind};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::helpers::any_over_expr;
|
||||
use crate::types::{BindingKind, Scope};
|
||||
use crate::visitor;
|
||||
use crate::visitor::Visitor;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
pub struct AllNamesFlags: u32 {
|
||||
const INVALID_FORMAT = 0b0000_0001;
|
||||
const INVALID_OBJECT = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the names bound to a given __all__ assignment.
|
||||
pub fn extract_all_names(
|
||||
ctx: &Context,
|
||||
stmt: &Stmt,
|
||||
scope: &Scope,
|
||||
) -> (Vec<String>, AllNamesFlags) {
|
||||
fn add_to_names(names: &mut Vec<String>, elts: &[Expr], flags: &mut AllNamesFlags) {
|
||||
for elt in elts {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &elt.node
|
||||
{
|
||||
names.push(value.to_string());
|
||||
} else {
|
||||
*flags |= AllNamesFlags::INVALID_OBJECT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_elts<'a>(
|
||||
ctx: &'a Context,
|
||||
expr: &'a Expr,
|
||||
) -> (Option<&'a Vec<Expr>>, AllNamesFlags) {
|
||||
match &expr.node {
|
||||
ExprKind::List { elts, .. } => {
|
||||
return (Some(elts), AllNamesFlags::empty());
|
||||
}
|
||||
ExprKind::Tuple { elts, .. } => {
|
||||
return (Some(elts), AllNamesFlags::empty());
|
||||
}
|
||||
ExprKind::ListComp { .. } => {
|
||||
// Allow comprehensions, even though we can't statically analyze them.
|
||||
return (None, AllNamesFlags::empty());
|
||||
}
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
..
|
||||
} => {
|
||||
// Allow `tuple()` and `list()` calls.
|
||||
if keywords.is_empty() && args.len() <= 1 {
|
||||
if ctx.resolve_call_path(func).map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["", "tuple"]
|
||||
|| call_path.as_slice() == ["", "list"]
|
||||
}) {
|
||||
if args.is_empty() {
|
||||
return (None, AllNamesFlags::empty());
|
||||
}
|
||||
match &args[0].node {
|
||||
ExprKind::List { elts, .. }
|
||||
| ExprKind::Set { elts, .. }
|
||||
| ExprKind::Tuple { elts, .. } => {
|
||||
return (Some(elts), AllNamesFlags::empty());
|
||||
}
|
||||
ExprKind::ListComp { .. }
|
||||
| ExprKind::SetComp { .. }
|
||||
| ExprKind::GeneratorExp { .. } => {
|
||||
// Allow comprehensions, even though we can't statically analyze
|
||||
// them.
|
||||
return (None, AllNamesFlags::empty());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(None, AllNamesFlags::INVALID_FORMAT)
|
||||
}
|
||||
|
||||
let mut names: Vec<String> = vec![];
|
||||
let mut flags = AllNamesFlags::empty();
|
||||
|
||||
// Grab the existing bound __all__ values.
|
||||
if let StmtKind::AugAssign { .. } = &stmt.node {
|
||||
if let Some(index) = scope.bindings.get("__all__") {
|
||||
if let BindingKind::Export(existing) = &ctx.bindings[*index].kind {
|
||||
names.extend_from_slice(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = match &stmt.node {
|
||||
StmtKind::Assign { value, .. } => Some(value),
|
||||
StmtKind::AnnAssign { value, .. } => value.as_ref(),
|
||||
StmtKind::AugAssign { value, .. } => Some(value),
|
||||
_ => None,
|
||||
} {
|
||||
if let ExprKind::BinOp { left, right, .. } = &value.node {
|
||||
let mut current_left = left;
|
||||
let mut current_right = right;
|
||||
loop {
|
||||
// Process the right side, which should be a "real" value.
|
||||
let (elts, new_flags) = extract_elts(ctx, current_right);
|
||||
flags |= new_flags;
|
||||
if let Some(elts) = elts {
|
||||
add_to_names(&mut names, elts, &mut flags);
|
||||
}
|
||||
|
||||
// Process the left side, which can be a "real" value or the "rest" of the
|
||||
// binary operation.
|
||||
if let ExprKind::BinOp { left, right, .. } = ¤t_left.node {
|
||||
current_left = left;
|
||||
current_right = right;
|
||||
} else {
|
||||
let (elts, new_flags) = extract_elts(ctx, current_left);
|
||||
flags |= new_flags;
|
||||
if let Some(elts) = elts {
|
||||
add_to_names(&mut names, elts, &mut flags);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (elts, new_flags) = extract_elts(ctx, value);
|
||||
flags |= new_flags;
|
||||
if let Some(elts) = elts {
|
||||
add_to_names(&mut names, elts, &mut flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(names, flags)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalVisitor<'a> {
|
||||
globals: FxHashMap<&'a str, &'a Stmt>,
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for GlobalVisitor<'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::Global { names } => {
|
||||
for name in names {
|
||||
self.globals.insert(name, stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::FunctionDef { .. }
|
||||
| StmtKind::AsyncFunctionDef { .. }
|
||||
| StmtKind::ClassDef { .. } => {
|
||||
// Don't recurse.
|
||||
}
|
||||
_ => visitor::walk_stmt(self, stmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a map from global name to its last-defining [`Stmt`].
|
||||
pub fn extract_globals(body: &[Stmt]) -> FxHashMap<&str, &Stmt> {
|
||||
let mut visitor = GlobalVisitor::default();
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
visitor.globals
|
||||
}
|
||||
|
||||
/// Check if a node is parent of a conditional branch.
|
||||
pub fn on_conditional_branch<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
|
||||
parents.any(|parent| {
|
||||
if matches!(
|
||||
parent.node,
|
||||
StmtKind::If { .. } | StmtKind::While { .. } | StmtKind::Match { .. }
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if let StmtKind::Expr { value } = &parent.node {
|
||||
if matches!(value.node, ExprKind::IfExp { .. }) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a node is in a nested block.
|
||||
pub fn in_nested_block<'a>(mut parents: impl Iterator<Item = &'a Stmt>) -> bool {
|
||||
parents.any(|parent| {
|
||||
matches!(
|
||||
parent.node,
|
||||
StmtKind::Try { .. }
|
||||
| StmtKind::TryStar { .. }
|
||||
| StmtKind::If { .. }
|
||||
| StmtKind::With { .. }
|
||||
| StmtKind::Match { .. }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a node represents an unpacking assignment.
|
||||
pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
|
||||
match &parent.node {
|
||||
StmtKind::With { items, .. } => items.iter().any(|item| {
|
||||
if let Some(optional_vars) = &item.optional_vars {
|
||||
if matches!(optional_vars.node, ExprKind::Tuple { .. }) {
|
||||
if any_over_expr(optional_vars, &|expr| expr == child) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}),
|
||||
StmtKind::Assign { targets, value, .. } => {
|
||||
// In `(a, b) = (1, 2)`, `(1, 2)` is the target, and it is a tuple.
|
||||
let value_is_tuple = matches!(
|
||||
&value.node,
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
|
||||
);
|
||||
// In `(a, b) = coords = (1, 2)`, `(a, b)` and `coords` are the targets, and
|
||||
// `(a, b)` is a tuple. (We use "tuple" as a placeholder for any
|
||||
// unpackable expression.)
|
||||
let targets_are_tuples = targets.iter().all(|item| {
|
||||
matches!(
|
||||
item.node,
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
|
||||
)
|
||||
});
|
||||
// If we're looking at `a` in `(a, b) = coords = (1, 2)`, then we should
|
||||
// identify that the current expression is in a tuple.
|
||||
let child_in_tuple = targets_are_tuples
|
||||
|| targets.iter().any(|item| {
|
||||
matches!(
|
||||
item.node,
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
|
||||
) && any_over_expr(item, &|expr| expr == child)
|
||||
});
|
||||
|
||||
// If our child is a tuple, and value is not, it's always an unpacking
|
||||
// expression. Ex) `x, y = tup`
|
||||
if child_in_tuple && !value_is_tuple {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If our child isn't a tuple, but value is, it's never an unpacking expression.
|
||||
// Ex) `coords = (1, 2)`
|
||||
if !child_in_tuple && value_is_tuple {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If our target and the value are both tuples, then it's an unpacking
|
||||
// expression assuming there's at least one non-tuple child.
|
||||
// Ex) Given `(x, y) = coords = 1, 2`, `(x, y)` is considered an unpacking
|
||||
// expression. Ex) Given `(x, y) = (a, b) = 1, 2`, `(x, y)` isn't
|
||||
// considered an unpacking expression.
|
||||
if child_in_tuple && value_is_tuple {
|
||||
return !targets_are_tuples;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;
|
||||
|
||||
/// Extract all [`Cmpop`] operators from a source code snippet, with appropriate
|
||||
/// ranges.
|
||||
///
|
||||
/// `RustPython` doesn't include line and column information on [`Cmpop`] nodes.
|
||||
/// `CPython` doesn't either. This method iterates over the token stream and
|
||||
/// re-identifies [`Cmpop`] nodes, annotating them with valid ranges.
|
||||
pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
|
||||
let mut tok_iter = lexer::lex(contents, Mode::Module).flatten().peekable();
|
||||
let mut ops: Vec<LocatedCmpop> = vec![];
|
||||
let mut count: usize = 0;
|
||||
loop {
|
||||
let Some((start, tok, end)) = tok_iter.next() else {
|
||||
break;
|
||||
};
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
count += 1;
|
||||
continue;
|
||||
} else if matches!(tok, Tok::Rpar) {
|
||||
count -= 1;
|
||||
continue;
|
||||
}
|
||||
if count == 0 {
|
||||
match tok {
|
||||
Tok::Not => {
|
||||
if let Some((_, _, end)) =
|
||||
tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::In))
|
||||
{
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::NotIn));
|
||||
}
|
||||
}
|
||||
Tok::In => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::In));
|
||||
}
|
||||
Tok::Is => {
|
||||
let op = if let Some((_, _, end)) =
|
||||
tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::Not))
|
||||
{
|
||||
LocatedCmpop::new(start, end, Cmpop::IsNot)
|
||||
} else {
|
||||
LocatedCmpop::new(start, end, Cmpop::Is)
|
||||
};
|
||||
ops.push(op);
|
||||
}
|
||||
Tok::NotEqual => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::NotEq));
|
||||
}
|
||||
Tok::EqEqual => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::Eq));
|
||||
}
|
||||
Tok::GreaterEqual => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::GtE));
|
||||
}
|
||||
Tok::Greater => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::Gt));
|
||||
}
|
||||
Tok::LessEqual => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::LtE));
|
||||
}
|
||||
Tok::Less => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::Lt));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::ast::{Cmpop, Location};
|
||||
|
||||
use crate::operations::{locate_cmpops, LocatedCmpop};
|
||||
|
||||
#[test]
|
||||
fn locates_cmpops() {
|
||||
assert_eq!(
|
||||
locate_cmpops("x == 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 4),
|
||||
Cmpop::Eq
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x != 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 4),
|
||||
Cmpop::NotEq
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x is 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 4),
|
||||
Cmpop::Is
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x is not 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 8),
|
||||
Cmpop::IsNot
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x in 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 4),
|
||||
Cmpop::In
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x not in 1"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 8),
|
||||
Cmpop::NotIn
|
||||
)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
locate_cmpops("x != (1 is not 2)"),
|
||||
vec![LocatedCmpop::new(
|
||||
Location::new(1, 2),
|
||||
Location::new(1, 4),
|
||||
Cmpop::NotEq
|
||||
)]
|
||||
);
|
||||
}
|
||||
}
|
142
crates/ruff_python_ast/src/relocate.rs
Normal file
142
crates/ruff_python_ast/src/relocate.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use rustpython_parser::ast::{Expr, ExprKind, Keyword};
|
||||
|
||||
use crate::types::Range;
|
||||
|
||||
fn relocate_keyword(keyword: &mut Keyword, location: Range) {
|
||||
keyword.location = location.location;
|
||||
keyword.end_location = Some(location.end_location);
|
||||
relocate_expr(&mut keyword.node.value, location);
|
||||
}
|
||||
|
||||
/// Change an expression's location (recursively) to match a desired, fixed
|
||||
/// location.
|
||||
pub fn relocate_expr(expr: &mut Expr, location: Range) {
|
||||
expr.location = location.location;
|
||||
expr.end_location = Some(location.end_location);
|
||||
match &mut expr.node {
|
||||
ExprKind::BoolOp { values, .. } => {
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::NamedExpr { target, value } => {
|
||||
relocate_expr(target, location);
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::BinOp { left, right, .. } => {
|
||||
relocate_expr(left, location);
|
||||
relocate_expr(right, location);
|
||||
}
|
||||
ExprKind::UnaryOp { operand, .. } => {
|
||||
relocate_expr(operand, location);
|
||||
}
|
||||
ExprKind::Lambda { body, .. } => {
|
||||
relocate_expr(body, location);
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
relocate_expr(test, location);
|
||||
relocate_expr(body, location);
|
||||
relocate_expr(orelse, location);
|
||||
}
|
||||
ExprKind::Dict { keys, values } => {
|
||||
for expr in keys.iter_mut().flatten() {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Set { elts } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::ListComp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::SetComp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::DictComp { key, value, .. } => {
|
||||
relocate_expr(key, location);
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::GeneratorExp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::Await { value } => relocate_expr(value, location),
|
||||
ExprKind::Yield { value } => {
|
||||
if let Some(expr) = value {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::YieldFrom { value } => relocate_expr(value, location),
|
||||
ExprKind::Compare {
|
||||
left, comparators, ..
|
||||
} => {
|
||||
relocate_expr(left, location);
|
||||
for expr in comparators {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
relocate_expr(func, location);
|
||||
for expr in args {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
for keyword in keywords {
|
||||
relocate_keyword(keyword, location);
|
||||
}
|
||||
}
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
relocate_expr(value, location);
|
||||
if let Some(expr) = format_spec {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::JoinedStr { values } => {
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Constant { .. } => {}
|
||||
ExprKind::Attribute { value, .. } => {
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::Subscript { value, slice, .. } => {
|
||||
relocate_expr(value, location);
|
||||
relocate_expr(slice, location);
|
||||
}
|
||||
ExprKind::Starred { value, .. } => {
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::Name { .. } => {}
|
||||
ExprKind::List { elts, .. } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Tuple { elts, .. } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
if let Some(expr) = lower {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
if let Some(expr) = upper {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
if let Some(expr) = step {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1580
crates/ruff_python_ast/src/source_code/generator.rs
Normal file
1580
crates/ruff_python_ast/src/source_code/generator.rs
Normal file
File diff suppressed because it is too large
Load diff
117
crates/ruff_python_ast/src/source_code/indexer.rs
Normal file
117
crates/ruff_python_ast/src/source_code/indexer.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
//! Struct used to index source code, to enable efficient lookup of tokens that
|
||||
//! are omitted from the AST (e.g., commented lines).
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
pub struct Indexer {
|
||||
commented_lines: Vec<usize>,
|
||||
continuation_lines: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Indexer {
|
||||
pub fn commented_lines(&self) -> &[usize] {
|
||||
&self.commented_lines
|
||||
}
|
||||
|
||||
pub fn continuation_lines(&self) -> &[usize] {
|
||||
&self.continuation_lines
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[LexResult]> for Indexer {
|
||||
fn from(lxr: &[LexResult]) -> Self {
|
||||
let mut commented_lines = Vec::new();
|
||||
let mut continuation_lines = Vec::new();
|
||||
let mut prev: Option<(&Location, &Tok, &Location)> = None;
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::Comment(_)) {
|
||||
commented_lines.push(start.row());
|
||||
}
|
||||
if let Some((.., prev_tok, prev_end)) = prev {
|
||||
if !matches!(
|
||||
prev_tok,
|
||||
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)
|
||||
) {
|
||||
for line in prev_end.row()..start.row() {
|
||||
continuation_lines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = Some((start, tok, end));
|
||||
}
|
||||
Self {
|
||||
commented_lines,
|
||||
continuation_lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::{lexer, Mode};
|
||||
|
||||
use crate::source_code::Indexer;
|
||||
|
||||
#[test]
|
||||
fn continuation() {
|
||||
let contents = r#"x = 1"#;
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer: Indexer = lxr.as_slice().into();
|
||||
assert_eq!(indexer.continuation_lines(), Vec::<usize>::new().as_slice());
|
||||
|
||||
let contents = r#"
|
||||
# Hello, world!
|
||||
|
||||
x = 1
|
||||
|
||||
y = 2
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer: Indexer = lxr.as_slice().into();
|
||||
assert_eq!(indexer.continuation_lines(), Vec::<usize>::new().as_slice());
|
||||
|
||||
let contents = r#"
|
||||
x = \
|
||||
1
|
||||
|
||||
if True:
|
||||
z = \
|
||||
\
|
||||
2
|
||||
|
||||
(
|
||||
"abc" # Foo
|
||||
"def" \
|
||||
"ghi"
|
||||
)
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer: Indexer = lxr.as_slice().into();
|
||||
assert_eq!(indexer.continuation_lines(), [1, 5, 6, 11]);
|
||||
|
||||
let contents = r#"
|
||||
x = 1; import sys
|
||||
import os
|
||||
|
||||
if True:
|
||||
x = 1; import sys
|
||||
import os
|
||||
|
||||
if True:
|
||||
x = 1; \
|
||||
import os
|
||||
|
||||
x = 1; \
|
||||
import os
|
||||
"#
|
||||
.trim();
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let indexer: Indexer = lxr.as_slice().into();
|
||||
assert_eq!(indexer.continuation_lines(), [9, 12]);
|
||||
}
|
||||
}
|
256
crates/ruff_python_ast/src/source_code/locator.rs
Normal file
256
crates/ruff_python_ast/src/source_code/locator.rs
Normal file
|
@ -0,0 +1,256 @@
|
|||
//! Struct used to efficiently slice source code at (row, column) Locations.
|
||||
|
||||
use once_cell::unsync::OnceCell;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::types::Range;
|
||||
|
||||
pub struct Locator<'a> {
|
||||
contents: &'a str,
|
||||
index: OnceCell<Index>,
|
||||
}
|
||||
|
||||
pub enum Index {
|
||||
Ascii(Vec<usize>),
|
||||
Utf8(Vec<Vec<usize>>),
|
||||
}
|
||||
|
||||
/// Compute the starting byte index of each line in ASCII source code.
|
||||
fn index_ascii(contents: &str) -> Vec<usize> {
|
||||
let mut index = Vec::with_capacity(48);
|
||||
index.push(0);
|
||||
let bytes = contents.as_bytes();
|
||||
for (i, byte) in bytes.iter().enumerate() {
|
||||
if *byte == b'\n' {
|
||||
index.push(i + 1);
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
/// Compute the starting byte index of each character in UTF-8 source code.
|
||||
fn index_utf8(contents: &str) -> Vec<Vec<usize>> {
|
||||
let mut index = Vec::with_capacity(48);
|
||||
let mut current_row = Vec::with_capacity(48);
|
||||
let mut current_byte_offset = 0;
|
||||
let mut previous_char = '\0';
|
||||
for char in contents.chars() {
|
||||
// Skip BOM.
|
||||
if previous_char == '\0' && char == '\u{feff}' {
|
||||
current_byte_offset += char.len_utf8();
|
||||
continue;
|
||||
}
|
||||
|
||||
current_row.push(current_byte_offset);
|
||||
if char == '\n' {
|
||||
if previous_char == '\r' {
|
||||
current_row.pop();
|
||||
}
|
||||
index.push(current_row);
|
||||
current_row = Vec::with_capacity(48);
|
||||
}
|
||||
current_byte_offset += char.len_utf8();
|
||||
previous_char = char;
|
||||
}
|
||||
index.push(current_row);
|
||||
index
|
||||
}
|
||||
|
||||
/// Compute the starting byte index of each line in source code.
|
||||
pub fn index(contents: &str) -> Index {
|
||||
if contents.is_ascii() {
|
||||
Index::Ascii(index_ascii(contents))
|
||||
} else {
|
||||
Index::Utf8(index_utf8(contents))
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate a [`Location`] to a byte offset in ASCII source code.
|
||||
fn truncate_ascii(location: Location, index: &[usize], contents: &str) -> usize {
|
||||
if location.row() - 1 == index.len() && location.column() == 0
|
||||
|| (!index.is_empty()
|
||||
&& location.row() - 1 == index.len() - 1
|
||||
&& index[location.row() - 1] + location.column() >= contents.len())
|
||||
{
|
||||
contents.len()
|
||||
} else {
|
||||
index[location.row() - 1] + location.column()
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate a [`Location`] to a byte offset in UTF-8 source code.
|
||||
fn truncate_utf8(location: Location, index: &[Vec<usize>], contents: &str) -> usize {
|
||||
if (location.row() - 1 == index.len() && location.column() == 0)
|
||||
|| (location.row() - 1 == index.len() - 1
|
||||
&& location.column() == index[location.row() - 1].len())
|
||||
{
|
||||
contents.len()
|
||||
} else {
|
||||
index[location.row() - 1][location.column()]
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate a [`Location`] to a byte offset in source code.
|
||||
fn truncate(location: Location, index: &Index, contents: &str) -> usize {
|
||||
match index {
|
||||
Index::Ascii(index) => truncate_ascii(location, index, contents),
|
||||
Index::Utf8(index) => truncate_utf8(location, index, contents),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Locator<'a> {
|
||||
pub const fn new(contents: &'a str) -> Self {
|
||||
Self {
|
||||
contents,
|
||||
index: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_init_index(&self) -> &Index {
|
||||
self.index.get_or_init(|| index(self.contents))
|
||||
}
|
||||
|
||||
/// Take the source code up to the given [`Location`].
|
||||
pub fn take(&self, location: Location) -> &'a str {
|
||||
let index = self.get_or_init_index();
|
||||
let offset = truncate(location, index, self.contents);
|
||||
&self.contents[..offset]
|
||||
}
|
||||
|
||||
/// Take the source code after the given [`Location`].
|
||||
pub fn skip(&self, location: Location) -> &'a str {
|
||||
let index = self.get_or_init_index();
|
||||
let offset = truncate(location, index, self.contents);
|
||||
&self.contents[offset..]
|
||||
}
|
||||
|
||||
/// Take the source code between the given [`Range`].
|
||||
pub fn slice(&self, range: Range) -> &'a str {
|
||||
let index = self.get_or_init_index();
|
||||
let start = truncate(range.location, index, self.contents);
|
||||
let end = truncate(range.end_location, index, self.contents);
|
||||
&self.contents[start..end]
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> usize {
|
||||
self.contents.len()
|
||||
}
|
||||
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.contents.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use super::{index_ascii, index_utf8, truncate_ascii, truncate_utf8};
|
||||
|
||||
#[test]
|
||||
fn ascii_index() {
|
||||
let contents = "";
|
||||
let index = index_ascii(contents);
|
||||
assert_eq!(index, [0]);
|
||||
|
||||
let contents = "x = 1";
|
||||
let index = index_ascii(contents);
|
||||
assert_eq!(index, [0]);
|
||||
|
||||
let contents = "x = 1\n";
|
||||
let index = index_ascii(contents);
|
||||
assert_eq!(index, [0, 6]);
|
||||
|
||||
let contents = "x = 1\r\n";
|
||||
let index = index_ascii(contents);
|
||||
assert_eq!(index, [0, 7]);
|
||||
|
||||
let contents = "x = 1\ny = 2\nz = x + y\n";
|
||||
let index = index_ascii(contents);
|
||||
assert_eq!(index, [0, 6, 12, 22]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_truncate() {
|
||||
let contents = "x = 1\ny = 2";
|
||||
let index = index_ascii(contents);
|
||||
|
||||
// First row.
|
||||
let loc = truncate_ascii(Location::new(1, 0), &index, contents);
|
||||
assert_eq!(loc, 0);
|
||||
|
||||
// Second row.
|
||||
let loc = truncate_ascii(Location::new(2, 0), &index, contents);
|
||||
assert_eq!(loc, 6);
|
||||
|
||||
// One-past-the-end.
|
||||
let loc = truncate_ascii(Location::new(3, 0), &index, contents);
|
||||
assert_eq!(loc, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_index() {
|
||||
let contents = "";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 1);
|
||||
assert_eq!(index[0], Vec::<usize>::new());
|
||||
|
||||
let contents = "x = 1";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 1);
|
||||
assert_eq!(index[0], [0, 1, 2, 3, 4]);
|
||||
|
||||
let contents = "x = 1\n";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 2);
|
||||
assert_eq!(index[0], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(index[1], Vec::<usize>::new());
|
||||
|
||||
let contents = "x = 1\r\n";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 2);
|
||||
assert_eq!(index[0], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(index[1], Vec::<usize>::new());
|
||||
|
||||
let contents = "x = 1\ny = 2\nz = x + y\n";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 4);
|
||||
assert_eq!(index[0], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(index[1], [6, 7, 8, 9, 10, 11]);
|
||||
assert_eq!(index[2], [12, 13, 14, 15, 16, 17, 18, 19, 20, 21]);
|
||||
assert_eq!(index[3], Vec::<usize>::new());
|
||||
|
||||
let contents = "# \u{4e9c}\nclass Foo:\n \"\"\".\"\"\"";
|
||||
let index = index_utf8(contents);
|
||||
assert_eq!(index.len(), 3);
|
||||
assert_eq!(index[0], [0, 1, 2, 5]);
|
||||
assert_eq!(index[1], [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
||||
assert_eq!(index[2], [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_truncate() {
|
||||
let contents = "x = '☃'\ny = 2";
|
||||
let index = index_utf8(contents);
|
||||
|
||||
// First row.
|
||||
let loc = truncate_utf8(Location::new(1, 0), &index, contents);
|
||||
assert_eq!(loc, 0);
|
||||
|
||||
let loc = truncate_utf8(Location::new(1, 5), &index, contents);
|
||||
assert_eq!(loc, 5);
|
||||
assert_eq!(&contents[loc..], "☃'\ny = 2");
|
||||
|
||||
let loc = truncate_utf8(Location::new(1, 6), &index, contents);
|
||||
assert_eq!(loc, 8);
|
||||
assert_eq!(&contents[loc..], "'\ny = 2");
|
||||
|
||||
// Second row.
|
||||
let loc = truncate_utf8(Location::new(2, 0), &index, contents);
|
||||
assert_eq!(loc, 10);
|
||||
|
||||
// One-past-the-end.
|
||||
let loc = truncate_utf8(Location::new(3, 0), &index, contents);
|
||||
assert_eq!(loc, 15);
|
||||
}
|
||||
}
|
21
crates/ruff_python_ast/src/source_code/mod.rs
Normal file
21
crates/ruff_python_ast/src/source_code/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
mod generator;
|
||||
mod indexer;
|
||||
mod locator;
|
||||
mod stylist;
|
||||
|
||||
pub use generator::Generator;
|
||||
pub use indexer::Indexer;
|
||||
pub use locator::Locator;
|
||||
use rustpython_parser as parser;
|
||||
use rustpython_parser::ParseError;
|
||||
pub use stylist::{LineEnding, Stylist};
|
||||
|
||||
/// Run round-trip source code generation on a given Python code.
|
||||
pub fn round_trip(code: &str, source_path: &str) -> Result<String, ParseError> {
|
||||
let locator = Locator::new(code);
|
||||
let python_ast = parser::parse_program(code, source_path)?;
|
||||
let stylist = Stylist::from_contents(code, &locator);
|
||||
let mut generator: Generator = (&stylist).into();
|
||||
generator.unparse_suite(&python_ast);
|
||||
Ok(generator.generate())
|
||||
}
|
319
crates/ruff_python_ast/src/source_code/stylist.rs
Normal file
319
crates/ruff_python_ast/src/source_code/stylist.rs
Normal file
|
@ -0,0 +1,319 @@
|
|||
//! Detect code style from Python source code.
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
use once_cell::unsync::OnceCell;
|
||||
use rustpython_parser::ast::Location;
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use crate::source_code::Locator;
|
||||
use ruff_rustpython::vendor;
|
||||
|
||||
use crate::strings::leading_quote;
|
||||
use crate::types::Range;
|
||||
|
||||
pub struct Stylist<'a> {
|
||||
contents: &'a str,
|
||||
locator: &'a Locator<'a>,
|
||||
indentation: OnceCell<Indentation>,
|
||||
quote: OnceCell<Quote>,
|
||||
line_ending: OnceCell<LineEnding>,
|
||||
}
|
||||
|
||||
impl<'a> Stylist<'a> {
|
||||
pub fn indentation(&'a self) -> &'a Indentation {
|
||||
self.indentation
|
||||
.get_or_init(|| detect_indentation(self.contents, self.locator).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn quote(&'a self) -> &'a Quote {
|
||||
self.quote
|
||||
.get_or_init(|| detect_quote(self.contents, self.locator).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn line_ending(&'a self) -> &'a LineEnding {
|
||||
self.line_ending
|
||||
.get_or_init(|| detect_line_ending(self.contents).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn from_contents(contents: &'a str, locator: &'a Locator<'a>) -> Self {
|
||||
Self {
|
||||
contents,
|
||||
locator,
|
||||
indentation: OnceCell::default(),
|
||||
quote: OnceCell::default(),
|
||||
line_ending: OnceCell::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The quotation style used in Python source code.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum Quote {
|
||||
Single,
|
||||
#[default]
|
||||
Double,
|
||||
}
|
||||
|
||||
impl From<Quote> for char {
|
||||
fn from(val: Quote) -> Self {
|
||||
match val {
|
||||
Quote::Single => '\'',
|
||||
Quote::Double => '"',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Quote> for vendor::str::Quote {
|
||||
fn from(val: &Quote) -> Self {
|
||||
match val {
|
||||
Quote::Single => vendor::str::Quote::Single,
|
||||
Quote::Double => vendor::str::Quote::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Quote {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Quote::Single => write!(f, "\'"),
|
||||
Quote::Double => write!(f, "\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Quote> for char {
|
||||
fn from(val: &Quote) -> Self {
|
||||
match val {
|
||||
Quote::Single => '\'',
|
||||
Quote::Double => '"',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The indentation style used in Python source code.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Indentation(String);
|
||||
|
||||
impl Indentation {
|
||||
pub const fn new(indentation: String) -> Self {
|
||||
Self(indentation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Indentation {
|
||||
fn default() -> Self {
|
||||
Indentation(" ".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Indentation {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn as_char(&self) -> char {
|
||||
self.0.chars().next().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Indentation {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// The line ending style used in Python source code.
|
||||
/// See <https://docs.python.org/3/reference/lexical_analysis.html#physical-lines>
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum LineEnding {
|
||||
Lf,
|
||||
Cr,
|
||||
CrLf,
|
||||
}
|
||||
|
||||
impl Default for LineEnding {
|
||||
fn default() -> Self {
|
||||
if cfg!(windows) {
|
||||
LineEnding::CrLf
|
||||
} else {
|
||||
LineEnding::Lf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LineEnding {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LineEnding::CrLf => "\r\n",
|
||||
LineEnding::Lf => "\n",
|
||||
LineEnding::Cr => "\r",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for LineEnding {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the indentation style of the given tokens.
|
||||
fn detect_indentation(contents: &str, locator: &Locator) -> Option<Indentation> {
|
||||
for (_start, tok, end) in lexer::lex(contents, Mode::Module).flatten() {
|
||||
if let Tok::Indent { .. } = tok {
|
||||
let start = Location::new(end.row(), 0);
|
||||
let whitespace = locator.slice(Range::new(start, end));
|
||||
return Some(Indentation(whitespace.to_string()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Detect the quotation style of the given tokens.
|
||||
fn detect_quote(contents: &str, locator: &Locator) -> Option<Quote> {
|
||||
for (start, tok, end) in lexer::lex(contents, Mode::Module).flatten() {
|
||||
if let Tok::String { .. } = tok {
|
||||
let content = locator.slice(Range::new(start, end));
|
||||
if let Some(pattern) = leading_quote(content) {
|
||||
if pattern.contains("\"\"\"") {
|
||||
continue;
|
||||
} else if pattern.contains('\'') {
|
||||
return Some(Quote::Single);
|
||||
} else if pattern.contains('"') {
|
||||
return Some(Quote::Double);
|
||||
}
|
||||
unreachable!("Expected string to start with a valid quote prefix")
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Detect the line ending style of the given contents.
|
||||
fn detect_line_ending(contents: &str) -> Option<LineEnding> {
|
||||
if let Some(position) = contents.find('\n') {
|
||||
let position = position.saturating_sub(1);
|
||||
return if let Some('\r') = contents.chars().nth(position) {
|
||||
Some(LineEnding::CrLf)
|
||||
} else {
|
||||
Some(LineEnding::Lf)
|
||||
};
|
||||
} else if contents.find('\r').is_some() {
|
||||
return Some(LineEnding::Cr);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::source_code::stylist::{
|
||||
detect_indentation, detect_line_ending, detect_quote, Indentation, LineEnding, Quote,
|
||||
};
|
||||
use crate::source_code::Locator;
|
||||
|
||||
#[test]
|
||||
fn indentation() {
|
||||
let contents = r#"x = 1"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_indentation(contents, &locator), None);
|
||||
|
||||
let contents = r#"
|
||||
if True:
|
||||
pass
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
detect_indentation(contents, &locator),
|
||||
Some(Indentation(" ".to_string()))
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
if True:
|
||||
pass
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
detect_indentation(contents, &locator),
|
||||
Some(Indentation(" ".to_string()))
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
if True:
|
||||
pass
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
detect_indentation(contents, &locator),
|
||||
Some(Indentation("\t".to_string()))
|
||||
);
|
||||
|
||||
// TODO(charlie): Should non-significant whitespace be detected?
|
||||
let contents = r#"
|
||||
x = (
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
)
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_indentation(contents, &locator), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote() {
|
||||
let contents = r#"x = 1"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), None);
|
||||
|
||||
let contents = r#"x = '1'"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), Some(Quote::Single));
|
||||
|
||||
let contents = r#"x = "1""#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), Some(Quote::Double));
|
||||
|
||||
let contents = r#"s = "It's done.""#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), Some(Quote::Double));
|
||||
|
||||
// No style if only double quoted docstring (will take default Double)
|
||||
let contents = r#"
|
||||
def f():
|
||||
"""Docstring."""
|
||||
pass
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), None);
|
||||
|
||||
// Detect from string literal appearing after docstring
|
||||
let contents = r#"
|
||||
"""Module docstring."""
|
||||
|
||||
a = 'v'
|
||||
"#;
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(detect_quote(contents, &locator), Some(Quote::Single));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_ending() {
|
||||
let contents = "x = 1";
|
||||
assert_eq!(detect_line_ending(contents), None);
|
||||
|
||||
let contents = "x = 1\n";
|
||||
assert_eq!(detect_line_ending(contents), Some(LineEnding::Lf));
|
||||
|
||||
let contents = "x = 1\r";
|
||||
assert_eq!(detect_line_ending(contents), Some(LineEnding::Cr));
|
||||
|
||||
let contents = "x = 1\r\n";
|
||||
assert_eq!(detect_line_ending(contents), Some(LineEnding::CrLf));
|
||||
}
|
||||
}
|
38
crates/ruff_python_ast/src/strings.rs
Normal file
38
crates/ruff_python_ast/src/strings.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use ruff_python_stdlib::str::{
|
||||
SINGLE_QUOTE_PREFIXES, SINGLE_QUOTE_SUFFIXES, TRIPLE_QUOTE_PREFIXES, TRIPLE_QUOTE_SUFFIXES,
|
||||
};
|
||||
|
||||
/// Strip the leading and trailing quotes from a docstring.
|
||||
pub fn raw_contents(contents: &str) -> &str {
|
||||
for pattern in TRIPLE_QUOTE_PREFIXES {
|
||||
if contents.starts_with(pattern) {
|
||||
return &contents[pattern.len()..contents.len() - 3];
|
||||
}
|
||||
}
|
||||
for pattern in SINGLE_QUOTE_PREFIXES {
|
||||
if contents.starts_with(pattern) {
|
||||
return &contents[pattern.len()..contents.len() - 1];
|
||||
}
|
||||
}
|
||||
unreachable!("Expected docstring to start with a valid triple- or single-quote prefix")
|
||||
}
|
||||
|
||||
/// Return the leading quote string for a docstring (e.g., `"""`).
|
||||
pub fn leading_quote(content: &str) -> Option<&str> {
|
||||
if let Some(first_line) = content.lines().next() {
|
||||
for pattern in TRIPLE_QUOTE_PREFIXES.iter().chain(SINGLE_QUOTE_PREFIXES) {
|
||||
if first_line.starts_with(pattern) {
|
||||
return Some(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the trailing quote string for a docstring (e.g., `"""`).
|
||||
pub fn trailing_quote(content: &str) -> Option<&&str> {
|
||||
TRIPLE_QUOTE_SUFFIXES
|
||||
.iter()
|
||||
.chain(SINGLE_QUOTE_SUFFIXES)
|
||||
.find(|&pattern| content.ends_with(pattern))
|
||||
}
|
279
crates/ruff_python_ast/src/types.rs
Normal file
279
crates/ruff_python_ast/src/types.rs
Normal file
|
@ -0,0 +1,279 @@
|
|||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Arguments, Expr, Keyword, Located, Location, Stmt};
|
||||
|
||||
fn id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Node<'a> {
|
||||
Stmt(&'a Stmt),
|
||||
Expr(&'a Expr),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Range {
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
}
|
||||
|
||||
impl Range {
|
||||
pub const fn new(location: Location, end_location: Location) -> Self {
|
||||
Self {
|
||||
location,
|
||||
end_location,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_located<T, U>(located: &Located<T, U>) -> Self {
|
||||
Range::new(located.location, located.end_location.unwrap())
|
||||
}
|
||||
|
||||
pub fn contains(&self, other: &Range) -> bool {
|
||||
self.location <= other.location && self.end_location >= other.end_location
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FunctionDef<'a> {
|
||||
// Properties derived from StmtKind::FunctionDef.
|
||||
pub name: &'a str,
|
||||
pub args: &'a Arguments,
|
||||
pub body: &'a [Stmt],
|
||||
pub decorator_list: &'a [Expr],
|
||||
// pub returns: Option<&'a Expr>,
|
||||
// pub type_comment: Option<&'a str>,
|
||||
// Scope-specific properties.
|
||||
// TODO(charlie): Create AsyncFunctionDef to mirror the AST.
|
||||
pub async_: bool,
|
||||
pub globals: FxHashMap<&'a str, &'a Stmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClassDef<'a> {
|
||||
// Properties derived from StmtKind::ClassDef.
|
||||
pub name: &'a str,
|
||||
pub bases: &'a [Expr],
|
||||
pub keywords: &'a [Keyword],
|
||||
// pub body: &'a [Stmt],
|
||||
pub decorator_list: &'a [Expr],
|
||||
// Scope-specific properties.
|
||||
pub globals: FxHashMap<&'a str, &'a Stmt>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lambda<'a> {
|
||||
pub args: &'a Arguments,
|
||||
pub body: &'a Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScopeKind<'a> {
|
||||
Class(ClassDef<'a>),
|
||||
Function(FunctionDef<'a>),
|
||||
Generator,
|
||||
Module,
|
||||
Lambda(Lambda<'a>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Scope<'a> {
|
||||
pub id: usize,
|
||||
pub kind: ScopeKind<'a>,
|
||||
pub import_starred: bool,
|
||||
pub uses_locals: bool,
|
||||
/// A map from bound name to binding index, for live bindings.
|
||||
pub bindings: FxHashMap<&'a str, usize>,
|
||||
/// A map from bound name to binding index, for bindings that were created
|
||||
/// in the scope but rebound (and thus overridden) later on in the same
|
||||
/// scope.
|
||||
pub rebounds: FxHashMap<&'a str, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl<'a> Scope<'a> {
|
||||
pub fn new(kind: ScopeKind<'a>) -> Self {
|
||||
Scope {
|
||||
id: id(),
|
||||
kind,
|
||||
import_starred: false,
|
||||
uses_locals: false,
|
||||
bindings: FxHashMap::default(),
|
||||
rebounds: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pyflakes defines the following binding hierarchy (via inheritance):
|
||||
// Binding
|
||||
// ExportBinding
|
||||
// Annotation
|
||||
// Argument
|
||||
// Assignment
|
||||
// NamedExprAssignment
|
||||
// Definition
|
||||
// FunctionDefinition
|
||||
// ClassDefinition
|
||||
// Builtin
|
||||
// Importation
|
||||
// SubmoduleImportation
|
||||
// ImportationFrom
|
||||
// StarImportation
|
||||
// FutureImportation
|
||||
|
||||
#[derive(Clone, Debug, is_macro::Is)]
|
||||
pub enum BindingKind<'a> {
|
||||
Annotation,
|
||||
Argument,
|
||||
Assignment,
|
||||
Binding,
|
||||
LoopVar,
|
||||
Global,
|
||||
Nonlocal,
|
||||
Builtin,
|
||||
ClassDefinition,
|
||||
FunctionDefinition,
|
||||
Export(Vec<String>),
|
||||
FutureImportation,
|
||||
StarImportation(Option<usize>, Option<String>),
|
||||
Importation(&'a str, &'a str),
|
||||
FromImportation(&'a str, String),
|
||||
SubmoduleImportation(&'a str, &'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Binding<'a> {
|
||||
pub kind: BindingKind<'a>,
|
||||
pub range: Range,
|
||||
/// The context in which the binding was created.
|
||||
pub context: ExecutionContext,
|
||||
/// The statement in which the [`Binding`] was defined.
|
||||
pub source: Option<RefEquality<'a, Stmt>>,
|
||||
/// Tuple of (scope index, range) indicating the scope and range at which
|
||||
/// the binding was last used in a runtime context.
|
||||
pub runtime_usage: Option<(usize, Range)>,
|
||||
/// Tuple of (scope index, range) indicating the scope and range at which
|
||||
/// the binding was last used in a typing-time context.
|
||||
pub typing_usage: Option<(usize, Range)>,
|
||||
/// Tuple of (scope index, range) indicating the scope and range at which
|
||||
/// the binding was last used in a synthetic context. This is used for
|
||||
/// (e.g.) `__future__` imports, explicit re-exports, and other bindings
|
||||
/// that should be considered used even if they're never referenced.
|
||||
pub synthetic_usage: Option<(usize, Range)>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone)]
|
||||
pub enum ExecutionContext {
|
||||
Runtime,
|
||||
Typing,
|
||||
}
|
||||
|
||||
impl<'a> Binding<'a> {
|
||||
pub fn mark_used(&mut self, scope: usize, range: Range, context: ExecutionContext) {
|
||||
match context {
|
||||
ExecutionContext::Runtime => self.runtime_usage = Some((scope, range)),
|
||||
ExecutionContext::Typing => self.typing_usage = Some((scope, range)),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn used(&self) -> bool {
|
||||
self.runtime_usage.is_some()
|
||||
|| self.synthetic_usage.is_some()
|
||||
|| self.typing_usage.is_some()
|
||||
}
|
||||
|
||||
pub const fn is_definition(&self) -> bool {
|
||||
matches!(
|
||||
self.kind,
|
||||
BindingKind::ClassDefinition
|
||||
| BindingKind::FunctionDefinition
|
||||
| BindingKind::Builtin
|
||||
| BindingKind::FutureImportation
|
||||
| BindingKind::StarImportation(..)
|
||||
| BindingKind::Importation(..)
|
||||
| BindingKind::FromImportation(..)
|
||||
| BindingKind::SubmoduleImportation(..)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn redefines(&self, existing: &'a Binding) -> bool {
|
||||
match &self.kind {
|
||||
BindingKind::Importation(.., full_name) => {
|
||||
if let BindingKind::SubmoduleImportation(.., existing) = &existing.kind {
|
||||
return full_name == existing;
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(.., full_name) => {
|
||||
if let BindingKind::SubmoduleImportation(.., existing) = &existing.kind {
|
||||
return full_name == existing;
|
||||
}
|
||||
}
|
||||
BindingKind::SubmoduleImportation(.., full_name) => match &existing.kind {
|
||||
BindingKind::Importation(.., existing)
|
||||
| BindingKind::SubmoduleImportation(.., existing) => {
|
||||
return full_name == existing;
|
||||
}
|
||||
BindingKind::FromImportation(.., existing) => {
|
||||
return full_name == existing;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
BindingKind::Annotation => {
|
||||
return false;
|
||||
}
|
||||
BindingKind::FutureImportation => {
|
||||
return false;
|
||||
}
|
||||
BindingKind::StarImportation(..) => {
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
existing.is_definition()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct RefEquality<'a, T>(pub &'a T);
|
||||
|
||||
impl<'a, T> std::hash::Hash for RefEquality<'a, T> {
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: std::hash::Hasher,
|
||||
{
|
||||
(self.0 as *const T).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T> PartialEq<RefEquality<'b, T>> for RefEquality<'a, T> {
|
||||
fn eq(&self, other: &RefEquality<'b, T>) -> bool {
|
||||
std::ptr::eq(self.0, other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Eq for RefEquality<'a, T> {}
|
||||
|
||||
impl<'a, T> Deref for RefEquality<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&RefEquality<'a, Stmt>> for &'a Stmt {
|
||||
fn from(r: &RefEquality<'a, Stmt>) -> Self {
|
||||
r.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&RefEquality<'a, Expr>> for &'a Expr {
|
||||
fn from(r: &RefEquality<'a, Expr>) -> Self {
|
||||
r.0
|
||||
}
|
||||
}
|
||||
|
||||
pub type CallPath<'a> = smallvec::SmallVec<[&'a str; 8]>;
|
73
crates/ruff_python_ast/src/typing.rs
Normal file
73
crates/ruff_python_ast/src/typing.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use ruff_python_stdlib::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS};
|
||||
use rustpython_parser::ast::{Expr, ExprKind};
|
||||
|
||||
use crate::types::CallPath;
|
||||
|
||||
pub enum Callable {
|
||||
ForwardRef,
|
||||
Cast,
|
||||
NewType,
|
||||
TypeVar,
|
||||
NamedTuple,
|
||||
TypedDict,
|
||||
MypyExtension,
|
||||
}
|
||||
|
||||
pub enum SubscriptKind {
|
||||
AnnotatedSubscript,
|
||||
PEP593AnnotatedSubscript,
|
||||
}
|
||||
|
||||
pub fn match_annotated_subscript<'a, F>(
|
||||
expr: &'a Expr,
|
||||
resolve_call_path: F,
|
||||
typing_modules: impl Iterator<Item = &'a str>,
|
||||
) -> Option<SubscriptKind>
|
||||
where
|
||||
F: FnOnce(&'a Expr) -> Option<CallPath<'a>>,
|
||||
{
|
||||
if !matches!(
|
||||
expr.node,
|
||||
ExprKind::Name { .. } | ExprKind::Attribute { .. }
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 = module.split('.').collect::<Vec<_>>();
|
||||
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<'a, F>(expr: &'a Expr, resolve_call_path: F) -> bool
|
||||
where
|
||||
F: FnOnce(&'a Expr) -> Option<CallPath<'a>>,
|
||||
{
|
||||
resolve_call_path(expr).map_or(false, |call_path| {
|
||||
PEP_585_BUILTINS_ELIGIBLE.contains(&call_path.as_slice())
|
||||
})
|
||||
}
|
219
crates/ruff_python_ast/src/visibility.rs
Normal file
219
crates/ruff_python_ast/src/visibility.rs
Normal file
|
@ -0,0 +1,219 @@
|
|||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::helpers::{collect_call_path, map_callable};
|
||||
use crate::types::CallPath;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Modifier {
|
||||
Module,
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Visibility {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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(path: &Path) -> Visibility {
|
||||
let mut components = path.iter().rev();
|
||||
|
||||
// Is the module itself private?
|
||||
// Ex) `_foo.py` (but not `__init__.py`)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Is the module in a private parent?
|
||||
// Ex) `_foo/bar.py`
|
||||
for component in components {
|
||||
let module_name = component.to_string_lossy();
|
||||
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| {
|
||||
let call_path = collect_call_path(expr);
|
||||
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"),
|
||||
}
|
||||
}
|
581
crates/ruff_python_ast/src/visitor.rs
Normal file
581
crates/ruff_python_ast/src/visitor.rs
Normal file
|
@ -0,0 +1,581 @@
|
|||
use rustpython_parser::ast::{
|
||||
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
|
||||
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern,
|
||||
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
|
||||
};
|
||||
|
||||
pub trait Visitor<'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
fn visit_annotation(&mut self, expr: &'a Expr) {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
fn visit_constant(&mut self, constant: &'a Constant) {
|
||||
walk_constant(self, constant);
|
||||
}
|
||||
fn visit_expr_context(&mut self, expr_context: &'a ExprContext) {
|
||||
walk_expr_context(self, expr_context);
|
||||
}
|
||||
fn visit_boolop(&mut self, boolop: &'a Boolop) {
|
||||
walk_boolop(self, boolop);
|
||||
}
|
||||
fn visit_operator(&mut self, operator: &'a Operator) {
|
||||
walk_operator(self, operator);
|
||||
}
|
||||
fn visit_unaryop(&mut self, unaryop: &'a Unaryop) {
|
||||
walk_unaryop(self, unaryop);
|
||||
}
|
||||
fn visit_cmpop(&mut self, cmpop: &'a Cmpop) {
|
||||
walk_cmpop(self, cmpop);
|
||||
}
|
||||
fn visit_comprehension(&mut self, comprehension: &'a Comprehension) {
|
||||
walk_comprehension(self, comprehension);
|
||||
}
|
||||
fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) {
|
||||
walk_excepthandler(self, excepthandler);
|
||||
}
|
||||
fn visit_format_spec(&mut self, format_spec: &'a Expr) {
|
||||
walk_expr(self, format_spec);
|
||||
}
|
||||
fn visit_arguments(&mut self, arguments: &'a Arguments) {
|
||||
walk_arguments(self, arguments);
|
||||
}
|
||||
fn visit_arg(&mut self, arg: &'a Arg) {
|
||||
walk_arg(self, arg);
|
||||
}
|
||||
fn visit_keyword(&mut self, keyword: &'a Keyword) {
|
||||
walk_keyword(self, keyword);
|
||||
}
|
||||
fn visit_alias(&mut self, alias: &'a Alias) {
|
||||
walk_alias(self, alias);
|
||||
}
|
||||
fn visit_withitem(&mut self, withitem: &'a Withitem) {
|
||||
walk_withitem(self, withitem);
|
||||
}
|
||||
fn visit_match_case(&mut self, match_case: &'a MatchCase) {
|
||||
walk_match_case(self, match_case);
|
||||
}
|
||||
fn visit_pattern(&mut self, pattern: &'a Pattern) {
|
||||
walk_pattern(self, pattern);
|
||||
}
|
||||
fn visit_body(&mut self, body: &'a [Stmt]) {
|
||||
walk_body(self, body);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_body<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, body: &'a [Stmt]) {
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef {
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_arguments(args);
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in returns {
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
StmtKind::AsyncFunctionDef {
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_arguments(args);
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in returns {
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
StmtKind::ClassDef {
|
||||
bases,
|
||||
keywords,
|
||||
body,
|
||||
decorator_list,
|
||||
..
|
||||
} => {
|
||||
for expr in bases {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for keyword in keywords {
|
||||
visitor.visit_keyword(keyword);
|
||||
}
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
StmtKind::Return { value } => {
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { targets } => {
|
||||
for expr in targets {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Assign { targets, value, .. } => {
|
||||
visitor.visit_expr(value);
|
||||
for expr in targets {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::AugAssign { target, op, value } => {
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_operator(op);
|
||||
visitor.visit_expr(value);
|
||||
}
|
||||
StmtKind::AnnAssign {
|
||||
target,
|
||||
annotation,
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_annotation(annotation);
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr(target);
|
||||
}
|
||||
StmtKind::For {
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(iter);
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_body(body);
|
||||
visitor.visit_body(orelse);
|
||||
}
|
||||
StmtKind::AsyncFor {
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(iter);
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_body(body);
|
||||
visitor.visit_body(orelse);
|
||||
}
|
||||
StmtKind::While { test, body, orelse } => {
|
||||
visitor.visit_expr(test);
|
||||
visitor.visit_body(body);
|
||||
visitor.visit_body(orelse);
|
||||
}
|
||||
StmtKind::If { test, body, orelse } => {
|
||||
visitor.visit_expr(test);
|
||||
visitor.visit_body(body);
|
||||
visitor.visit_body(orelse);
|
||||
}
|
||||
StmtKind::With { items, body, .. } => {
|
||||
for withitem in items {
|
||||
visitor.visit_withitem(withitem);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
StmtKind::AsyncWith { items, body, .. } => {
|
||||
for withitem in items {
|
||||
visitor.visit_withitem(withitem);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
StmtKind::Match { subject, cases } => {
|
||||
visitor.visit_expr(subject);
|
||||
for match_case in cases {
|
||||
visitor.visit_match_case(match_case);
|
||||
}
|
||||
}
|
||||
StmtKind::Raise { exc, cause } => {
|
||||
if let Some(expr) = exc {
|
||||
visitor.visit_expr(expr);
|
||||
};
|
||||
if let Some(expr) = cause {
|
||||
visitor.visit_expr(expr);
|
||||
};
|
||||
}
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
visitor.visit_body(body);
|
||||
for excepthandler in handlers {
|
||||
visitor.visit_excepthandler(excepthandler);
|
||||
}
|
||||
visitor.visit_body(orelse);
|
||||
visitor.visit_body(finalbody);
|
||||
}
|
||||
StmtKind::TryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
visitor.visit_body(body);
|
||||
for excepthandler in handlers {
|
||||
visitor.visit_excepthandler(excepthandler);
|
||||
}
|
||||
visitor.visit_body(orelse);
|
||||
visitor.visit_body(finalbody);
|
||||
}
|
||||
StmtKind::Assert { test, msg } => {
|
||||
visitor.visit_expr(test);
|
||||
if let Some(expr) = msg {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Import { names } => {
|
||||
for alias in names {
|
||||
visitor.visit_alias(alias);
|
||||
}
|
||||
}
|
||||
StmtKind::ImportFrom { names, .. } => {
|
||||
for alias in names {
|
||||
visitor.visit_alias(alias);
|
||||
}
|
||||
}
|
||||
StmtKind::Global { .. } => {}
|
||||
StmtKind::Nonlocal { .. } => {}
|
||||
StmtKind::Expr { value } => visitor.visit_expr(value),
|
||||
StmtKind::Pass => {}
|
||||
StmtKind::Break => {}
|
||||
StmtKind::Continue => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
|
||||
match &expr.node {
|
||||
ExprKind::BoolOp { op, values } => {
|
||||
visitor.visit_boolop(op);
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::NamedExpr { target, value } => {
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr(target);
|
||||
}
|
||||
ExprKind::BinOp { left, op, right } => {
|
||||
visitor.visit_expr(left);
|
||||
visitor.visit_operator(op);
|
||||
visitor.visit_expr(right);
|
||||
}
|
||||
ExprKind::UnaryOp { op, operand } => {
|
||||
visitor.visit_unaryop(op);
|
||||
visitor.visit_expr(operand);
|
||||
}
|
||||
ExprKind::Lambda { args, body } => {
|
||||
visitor.visit_arguments(args);
|
||||
visitor.visit_expr(body);
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
visitor.visit_expr(test);
|
||||
visitor.visit_expr(body);
|
||||
visitor.visit_expr(orelse);
|
||||
}
|
||||
ExprKind::Dict { keys, values } => {
|
||||
for expr in keys.iter().flatten() {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Set { elts } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::ListComp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::SetComp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::DictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
} => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(key);
|
||||
visitor.visit_expr(value);
|
||||
}
|
||||
ExprKind::GeneratorExp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::Await { value } => visitor.visit_expr(value),
|
||||
ExprKind::Yield { value } => {
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} => {
|
||||
visitor.visit_expr(left);
|
||||
for cmpop in ops {
|
||||
visitor.visit_cmpop(cmpop);
|
||||
}
|
||||
for expr in comparators {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
visitor.visit_expr(func);
|
||||
for expr in args {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for keyword in keywords {
|
||||
visitor.visit_keyword(keyword);
|
||||
}
|
||||
}
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
visitor.visit_expr(value);
|
||||
if let Some(expr) = format_spec {
|
||||
visitor.visit_format_spec(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::JoinedStr { values } => {
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
|
||||
ExprKind::Attribute { value, ctx, .. } => {
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Subscript { value, slice, ctx } => {
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr(slice);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Starred { value, ctx } => {
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Name { ctx, .. } => {
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::List { elts, ctx } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Tuple { elts, ctx } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
if let Some(expr) = lower {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = upper {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = step {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_constant<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, constant: &'a Constant) {
|
||||
if let Constant::Tuple(constants) = constant {
|
||||
for constant in constants {
|
||||
visitor.visit_constant(constant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
comprehension: &'a Comprehension,
|
||||
) {
|
||||
visitor.visit_expr(&comprehension.iter);
|
||||
visitor.visit_expr(&comprehension.target);
|
||||
for expr in &comprehension.ifs {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
excepthandler: &'a Excepthandler,
|
||||
) {
|
||||
match &excepthandler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
|
||||
if let Some(expr) = type_ {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_body(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
|
||||
for arg in &arguments.posonlyargs {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for arg in &arguments.args {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for arg in &arguments.kwonlyargs {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for expr in &arguments.kw_defaults {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for expr in &arguments.defaults {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) {
|
||||
visitor.visit_expr(&keyword.node.value);
|
||||
}
|
||||
|
||||
pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) {
|
||||
visitor.visit_expr(&withitem.context_expr);
|
||||
if let Some(expr) = &withitem.optional_vars {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_match_case<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, match_case: &'a MatchCase) {
|
||||
visitor.visit_pattern(&match_case.pattern);
|
||||
if let Some(expr) = &match_case.guard {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_body(&match_case.body);
|
||||
}
|
||||
|
||||
pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a Pattern) {
|
||||
match &pattern.node {
|
||||
PatternKind::MatchValue { value } => visitor.visit_expr(value),
|
||||
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
|
||||
PatternKind::MatchSequence { patterns } => {
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchMapping { keys, patterns, .. } => {
|
||||
for expr in keys {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchClass {
|
||||
cls,
|
||||
patterns,
|
||||
kwd_patterns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(cls);
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
|
||||
for pattern in kwd_patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchStar { .. } => {}
|
||||
PatternKind::MatchAs { pattern, .. } => {
|
||||
if let Some(pattern) = pattern {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchOr { patterns } => {
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
expr_context: &'a ExprContext,
|
||||
) {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {}
|
76
crates/ruff_python_ast/src/whitespace.rs
Normal file
76
crates/ruff_python_ast/src/whitespace.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::str::Lines;
|
||||
|
||||
use rustpython_parser::ast::{Located, Location};
|
||||
|
||||
use crate::source_code::Locator;
|
||||
use crate::types::Range;
|
||||
|
||||
/// Extract the leading indentation from a line.
|
||||
pub fn indentation<'a, T>(locator: &'a Locator, located: &'a Located<T>) -> Option<&'a str> {
|
||||
let range = Range::from_located(located);
|
||||
let indentation = locator.slice(Range::new(
|
||||
Location::new(range.location.row(), 0),
|
||||
Location::new(range.location.row(), range.location.column()),
|
||||
));
|
||||
if indentation.chars().all(char::is_whitespace) {
|
||||
Some(indentation)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the leading words from a line of text.
|
||||
pub fn leading_words(line: &str) -> &str {
|
||||
let line = line.trim();
|
||||
line.find(|char: char| !char.is_alphanumeric() && !char.is_whitespace())
|
||||
.map_or(line, |index| &line[..index])
|
||||
}
|
||||
|
||||
/// Extract the leading whitespace from a line of text.
|
||||
pub fn leading_space(line: &str) -> &str {
|
||||
line.find(|char: char| !char.is_whitespace())
|
||||
.map_or(line, |index| &line[..index])
|
||||
}
|
||||
|
||||
/// Replace any non-whitespace characters from an indentation string.
|
||||
pub fn clean(indentation: &str) -> String {
|
||||
indentation
|
||||
.chars()
|
||||
.map(|char| if char.is_whitespace() { char } else { ' ' })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Like `str#lines`, but includes a trailing newline as an empty line.
|
||||
pub struct LinesWithTrailingNewline<'a> {
|
||||
trailing: Option<&'a str>,
|
||||
underlying: Lines<'a>,
|
||||
}
|
||||
|
||||
impl<'a> LinesWithTrailingNewline<'a> {
|
||||
pub fn from(input: &'a str) -> LinesWithTrailingNewline<'a> {
|
||||
LinesWithTrailingNewline {
|
||||
underlying: input.lines(),
|
||||
trailing: if input.ends_with('\n') {
|
||||
Some("")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LinesWithTrailingNewline<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<&'a str> {
|
||||
let mut next = self.underlying.next();
|
||||
if next.is_none() {
|
||||
if self.trailing.is_some() {
|
||||
next = self.trailing;
|
||||
self.trailing = None;
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue