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:
Charlie Marsh 2023-03-07 10:18:40 -05:00 committed by GitHub
parent a5d302fcbf
commit bad6bdda1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
405 changed files with 1336 additions and 988 deletions

View 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
}

View 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"),
}
}

View 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,
}
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

File diff suppressed because it is too large Load diff

View 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;

View 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,
}
}
}

View 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, .. } = &current_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
)]
);
}
}

View 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);
}
}
}
}

File diff suppressed because it is too large Load diff

View 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]);
}
}

View 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);
}
}

View 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())
}

View 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));
}
}

View 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))
}

View 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]>;

View 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())
})
}

View 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"),
}
}

View 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) {}

View 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
}
}