use super::module::Mod;
use crate::{PyResult, VirtualMachine};
use ruff_python_ast as ast;
fn expr_context_name(ctx: ast::ExprContext) -> &'static str {
match ctx {
ast::ExprContext::Load => "Load",
ast::ExprContext::Store => "Store",
ast::ExprContext::Del => "Del",
ast::ExprContext::Invalid => "Invalid",
}
}
fn validate_name(vm: &VirtualMachine, name: &ast::name::Name) -> PyResult<()> {
match name.as_str() {
"None" | "True" | "False" => Err(vm.new_value_error(format!(
"identifier field can't represent '{}' constant",
name.as_str()
))),
_ => Ok(()),
}
}
fn validate_comprehension(vm: &VirtualMachine, gens: &[ast::Comprehension]) -> PyResult<()> {
if gens.is_empty() {
return Err(vm.new_value_error("comprehension with no generators"));
}
for comp in gens {
validate_expr(vm, &comp.target, ast::ExprContext::Store)?;
validate_expr(vm, &comp.iter, ast::ExprContext::Load)?;
validate_exprs(vm, &comp.ifs, ast::ExprContext::Load, false)?;
}
Ok(())
}
fn validate_keywords(vm: &VirtualMachine, keywords: &[ast::Keyword]) -> PyResult<()> {
for keyword in keywords {
validate_expr(vm, &keyword.value, ast::ExprContext::Load)?;
}
Ok(())
}
fn validate_parameters(vm: &VirtualMachine, params: &ast::Parameters) -> PyResult<()> {
for param in params
.posonlyargs
.iter()
.chain(¶ms.args)
.chain(¶ms.kwonlyargs)
{
if let Some(annotation) = ¶m.parameter.annotation {
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
if let Some(default) = ¶m.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
if let Some(vararg) = ¶ms.vararg
&& let Some(annotation) = &vararg.annotation
{
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
if let Some(kwarg) = ¶ms.kwarg
&& let Some(annotation) = &kwarg.annotation
{
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
Ok(())
}
fn validate_nonempty_seq(
vm: &VirtualMachine,
len: usize,
what: &'static str,
owner: &'static str,
) -> PyResult<()> {
if len == 0 {
return Err(vm.new_value_error(format!("empty {what} on {owner}")));
}
Ok(())
}
fn validate_assignlist(
vm: &VirtualMachine,
targets: &[ast::Expr],
ctx: ast::ExprContext,
) -> PyResult<()> {
validate_nonempty_seq(
vm,
targets.len(),
"targets",
if ctx == ast::ExprContext::Del {
"Delete"
} else {
"Assign"
},
)?;
validate_exprs(vm, targets, ctx, false)
}
fn validate_body(vm: &VirtualMachine, body: &[ast::Stmt], owner: &'static str) -> PyResult<()> {
validate_nonempty_seq(vm, body.len(), "body", owner)?;
validate_stmts(vm, body)
}
fn validate_interpolated_elements<'a>(
vm: &VirtualMachine,
elements: impl IntoIterator<Item = ast::InterpolatedStringElementRef<'a>>,
) -> PyResult<()> {
for element in elements {
if let ast::InterpolatedStringElementRef::Interpolation(interpolation) = element {
validate_expr(vm, &interpolation.expression, ast::ExprContext::Load)?;
if let Some(format_spec) = &interpolation.format_spec {
for spec_element in &format_spec.elements {
if let ast::InterpolatedStringElement::Interpolation(spec_interp) = spec_element
{
validate_expr(vm, &spec_interp.expression, ast::ExprContext::Load)?;
}
}
}
}
}
Ok(())
}
fn validate_pattern_match_value(vm: &VirtualMachine, expr: &ast::Expr) -> PyResult<()> {
validate_expr(vm, expr, ast::ExprContext::Load)?;
match expr {
ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => {
Ok(())
}
ast::Expr::Attribute(_) => Ok(()),
ast::Expr::UnaryOp(op) => match &*op.operand {
ast::Expr::NumberLiteral(_) => Ok(()),
_ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")),
},
ast::Expr::BinOp(bin) => match (&*bin.left, &*bin.right) {
(ast::Expr::NumberLiteral(_), ast::Expr::NumberLiteral(_)) => Ok(()),
_ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")),
},
ast::Expr::FString(_) | ast::Expr::TString(_) => Ok(()),
ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_) => {
Err(vm.new_value_error("unexpected constant inside of a literal pattern"))
}
_ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")),
}
}
fn validate_capture(vm: &VirtualMachine, name: &ast::Identifier) -> PyResult<()> {
if name.as_str() == "_" {
return Err(vm.new_value_error("can't capture name '_' in patterns"));
}
validate_name(vm, name.id())
}
fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool) -> PyResult<()> {
match pattern {
ast::Pattern::MatchValue(value) => validate_pattern_match_value(vm, &value.value),
ast::Pattern::MatchSingleton(singleton) => match singleton.value {
ast::Singleton::None | ast::Singleton::True | ast::Singleton::False => Ok(()),
},
ast::Pattern::MatchSequence(seq) => validate_patterns(vm, &seq.patterns, true),
ast::Pattern::MatchMapping(mapping) => {
if mapping.keys.len() != mapping.patterns.len() {
return Err(vm.new_value_error(
"MatchMapping doesn't have the same number of keys as patterns",
));
}
if let Some(rest) = &mapping.rest {
validate_capture(vm, rest)?;
}
for key in &mapping.keys {
if let ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) = key {
continue;
}
validate_pattern_match_value(vm, key)?;
}
validate_patterns(vm, &mapping.patterns, false)
}
ast::Pattern::MatchClass(match_class) => {
validate_expr(vm, &match_class.cls, ast::ExprContext::Load)?;
let mut cls = match_class.cls.as_ref();
loop {
match cls {
ast::Expr::Name(_) => break,
ast::Expr::Attribute(attr) => {
cls = &attr.value;
}
_ => {
return Err(vm.new_value_error(
"MatchClass cls field can only contain Name or Attribute nodes.",
));
}
}
}
for keyword in &match_class.arguments.keywords {
validate_name(vm, keyword.attr.id())?;
}
validate_patterns(vm, &match_class.arguments.patterns, false)?;
for keyword in &match_class.arguments.keywords {
validate_pattern(vm, &keyword.pattern, false)?;
}
Ok(())
}
ast::Pattern::MatchStar(star) => {
if !star_ok {
return Err(vm.new_value_error("can't use MatchStar here"));
}
if let Some(name) = &star.name {
validate_capture(vm, name)?;
}
Ok(())
}
ast::Pattern::MatchAs(match_as) => {
if let Some(name) = &match_as.name {
validate_capture(vm, name)?;
}
match &match_as.pattern {
None => Ok(()),
Some(pattern) => {
if match_as.name.is_none() {
return Err(vm.new_value_error(
"MatchAs must specify a target name if a pattern is given",
));
}
validate_pattern(vm, pattern, false)
}
}
}
ast::Pattern::MatchOr(match_or) => {
if match_or.patterns.len() < 2 {
return Err(vm.new_value_error("MatchOr requires at least 2 patterns"));
}
validate_patterns(vm, &match_or.patterns, false)
}
}
}
fn validate_patterns(
vm: &VirtualMachine,
patterns: &[ast::Pattern],
star_ok: bool,
) -> PyResult<()> {
for pattern in patterns {
validate_pattern(vm, pattern, star_ok)?;
}
Ok(())
}
fn validate_typeparam(vm: &VirtualMachine, tp: &ast::TypeParam) -> PyResult<()> {
match tp {
ast::TypeParam::TypeVar(tp) => {
validate_name(vm, tp.name.id())?;
if let Some(bound) = &tp.bound {
validate_expr(vm, bound, ast::ExprContext::Load)?;
}
if let Some(default) = &tp.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
ast::TypeParam::ParamSpec(tp) => {
validate_name(vm, tp.name.id())?;
if let Some(default) = &tp.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
ast::TypeParam::TypeVarTuple(tp) => {
validate_name(vm, tp.name.id())?;
if let Some(default) = &tp.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
}
Ok(())
}
fn validate_type_params(
vm: &VirtualMachine,
type_params: &Option<Box<ast::TypeParams>>,
) -> PyResult<()> {
if let Some(type_params) = type_params {
for tp in &type_params.type_params {
validate_typeparam(vm, tp)?;
}
}
Ok(())
}
fn validate_exprs(
vm: &VirtualMachine,
exprs: &[ast::Expr],
ctx: ast::ExprContext,
_null_ok: bool,
) -> PyResult<()> {
for expr in exprs {
validate_expr(vm, expr, ctx)?;
}
Ok(())
}
fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -> PyResult<()> {
let mut check_ctx = true;
let actual_ctx = match expr {
ast::Expr::Attribute(attr) => attr.ctx,
ast::Expr::Subscript(sub) => sub.ctx,
ast::Expr::Starred(star) => star.ctx,
ast::Expr::Name(name) => {
validate_name(vm, name.id())?;
name.ctx
}
ast::Expr::List(list) => list.ctx,
ast::Expr::Tuple(tuple) => tuple.ctx,
_ => {
if ctx != ast::ExprContext::Load {
return Err(vm.new_value_error(format!(
"expression which can't be assigned to in {} context",
expr_context_name(ctx)
)));
}
check_ctx = false;
ast::ExprContext::Invalid
}
};
if check_ctx && actual_ctx != ctx {
return Err(vm.new_value_error(format!(
"expression must have {} context but has {} instead",
expr_context_name(ctx),
expr_context_name(actual_ctx)
)));
}
match expr {
ast::Expr::BoolOp(op) => {
if op.values.len() < 2 {
return Err(vm.new_value_error("BoolOp with less than 2 values"));
}
validate_exprs(vm, &op.values, ast::ExprContext::Load, false)
}
ast::Expr::Named(named) => {
if !matches!(&*named.target, ast::Expr::Name(_)) {
return Err(vm.new_type_error("NamedExpr target must be a Name"));
}
validate_expr(vm, &named.value, ast::ExprContext::Load)
}
ast::Expr::BinOp(bin) => {
validate_expr(vm, &bin.left, ast::ExprContext::Load)?;
validate_expr(vm, &bin.right, ast::ExprContext::Load)
}
ast::Expr::UnaryOp(unary) => validate_expr(vm, &unary.operand, ast::ExprContext::Load),
ast::Expr::Lambda(lambda) => {
if let Some(parameters) = &lambda.parameters {
validate_parameters(vm, parameters)?;
}
validate_expr(vm, &lambda.body, ast::ExprContext::Load)
}
ast::Expr::If(ifexp) => {
validate_expr(vm, &ifexp.test, ast::ExprContext::Load)?;
validate_expr(vm, &ifexp.body, ast::ExprContext::Load)?;
validate_expr(vm, &ifexp.orelse, ast::ExprContext::Load)
}
ast::Expr::Dict(dict) => {
for item in &dict.items {
if let Some(key) = &item.key {
validate_expr(vm, key, ast::ExprContext::Load)?;
}
validate_expr(vm, &item.value, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Expr::Set(set) => validate_exprs(vm, &set.elts, ast::ExprContext::Load, false),
ast::Expr::ListComp(list) => {
validate_comprehension(vm, &list.generators)?;
validate_expr(vm, &list.elt, ast::ExprContext::Load)
}
ast::Expr::SetComp(set) => {
validate_comprehension(vm, &set.generators)?;
validate_expr(vm, &set.elt, ast::ExprContext::Load)
}
ast::Expr::DictComp(dict) => {
validate_comprehension(vm, &dict.generators)?;
validate_expr(vm, &dict.key, ast::ExprContext::Load)?;
validate_expr(vm, &dict.value, ast::ExprContext::Load)
}
ast::Expr::Generator(generator) => {
validate_comprehension(vm, &generator.generators)?;
validate_expr(vm, &generator.elt, ast::ExprContext::Load)
}
ast::Expr::Yield(yield_expr) => {
if let Some(value) = &yield_expr.value {
validate_expr(vm, value, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Expr::YieldFrom(yield_expr) => {
validate_expr(vm, &yield_expr.value, ast::ExprContext::Load)
}
ast::Expr::Await(await_expr) => {
validate_expr(vm, &await_expr.value, ast::ExprContext::Load)
}
ast::Expr::Compare(compare) => {
if compare.comparators.is_empty() {
return Err(vm.new_value_error("Compare with no comparators"));
}
if compare.comparators.len() != compare.ops.len() {
return Err(vm.new_value_error(
"Compare has a different number of comparators and operands",
));
}
validate_exprs(vm, &compare.comparators, ast::ExprContext::Load, false)?;
validate_expr(vm, &compare.left, ast::ExprContext::Load)
}
ast::Expr::Call(call) => {
validate_expr(vm, &call.func, ast::ExprContext::Load)?;
validate_exprs(vm, &call.arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &call.arguments.keywords)
}
ast::Expr::FString(fstring) => validate_interpolated_elements(
vm,
fstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
),
ast::Expr::TString(tstring) => validate_interpolated_elements(
vm,
tstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
),
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_) => Ok(()),
ast::Expr::Attribute(attr) => validate_expr(vm, &attr.value, ast::ExprContext::Load),
ast::Expr::Subscript(sub) => {
validate_expr(vm, &sub.slice, ast::ExprContext::Load)?;
validate_expr(vm, &sub.value, ast::ExprContext::Load)
}
ast::Expr::Starred(star) => validate_expr(vm, &star.value, ctx),
ast::Expr::Name(_) => Ok(()),
ast::Expr::List(list) => validate_exprs(vm, &list.elts, ctx, false),
ast::Expr::Tuple(tuple) => validate_exprs(vm, &tuple.elts, ctx, false),
ast::Expr::Slice(slice) => {
if let Some(lower) = &slice.lower {
validate_expr(vm, lower, ast::ExprContext::Load)?;
}
if let Some(upper) = &slice.upper {
validate_expr(vm, upper, ast::ExprContext::Load)?;
}
if let Some(step) = &slice.step {
validate_expr(vm, step, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Expr::IpyEscapeCommand(_) => Ok(()),
}
}
fn validate_decorators(vm: &VirtualMachine, decorators: &[ast::Decorator]) -> PyResult<()> {
for decorator in decorators {
validate_expr(vm, &decorator.expression, ast::ExprContext::Load)?;
}
Ok(())
}
fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
match stmt {
ast::Stmt::FunctionDef(func) => {
let owner = if func.is_async {
"AsyncFunctionDef"
} else {
"FunctionDef"
};
validate_body(vm, &func.body, owner)?;
validate_type_params(vm, &func.type_params)?;
validate_parameters(vm, &func.parameters)?;
validate_decorators(vm, &func.decorator_list)?;
if let Some(returns) = &func.returns {
validate_expr(vm, returns, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Stmt::ClassDef(class_def) => {
validate_body(vm, &class_def.body, "ClassDef")?;
validate_type_params(vm, &class_def.type_params)?;
if let Some(arguments) = &class_def.arguments {
validate_exprs(vm, &arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &arguments.keywords)?;
}
validate_decorators(vm, &class_def.decorator_list)
}
ast::Stmt::Return(ret) => {
if let Some(value) = &ret.value {
validate_expr(vm, value, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Stmt::Delete(del) => validate_assignlist(vm, &del.targets, ast::ExprContext::Del),
ast::Stmt::Assign(assign) => {
validate_assignlist(vm, &assign.targets, ast::ExprContext::Store)?;
validate_expr(vm, &assign.value, ast::ExprContext::Load)
}
ast::Stmt::AugAssign(assign) => {
validate_expr(vm, &assign.target, ast::ExprContext::Store)?;
validate_expr(vm, &assign.value, ast::ExprContext::Load)
}
ast::Stmt::AnnAssign(assign) => {
if assign.simple && !matches!(&*assign.target, ast::Expr::Name(_)) {
return Err(vm.new_type_error("AnnAssign with simple non-Name target"));
}
validate_expr(vm, &assign.target, ast::ExprContext::Store)?;
if let Some(value) = &assign.value {
validate_expr(vm, value, ast::ExprContext::Load)?;
}
validate_expr(vm, &assign.annotation, ast::ExprContext::Load)
}
ast::Stmt::TypeAlias(alias) => {
if !matches!(&*alias.name, ast::Expr::Name(_)) {
return Err(vm.new_type_error("TypeAlias with non-Name name"));
}
validate_expr(vm, &alias.name, ast::ExprContext::Store)?;
validate_type_params(vm, &alias.type_params)?;
validate_expr(vm, &alias.value, ast::ExprContext::Load)
}
ast::Stmt::For(for_stmt) => {
let owner = if for_stmt.is_async { "AsyncFor" } else { "For" };
validate_expr(vm, &for_stmt.target, ast::ExprContext::Store)?;
validate_expr(vm, &for_stmt.iter, ast::ExprContext::Load)?;
validate_body(vm, &for_stmt.body, owner)?;
validate_stmts(vm, &for_stmt.orelse)
}
ast::Stmt::While(while_stmt) => {
validate_expr(vm, &while_stmt.test, ast::ExprContext::Load)?;
validate_body(vm, &while_stmt.body, "While")?;
validate_stmts(vm, &while_stmt.orelse)
}
ast::Stmt::If(if_stmt) => {
validate_expr(vm, &if_stmt.test, ast::ExprContext::Load)?;
validate_body(vm, &if_stmt.body, "If")?;
for clause in &if_stmt.elif_else_clauses {
if let Some(test) = &clause.test {
validate_expr(vm, test, ast::ExprContext::Load)?;
}
validate_body(vm, &clause.body, "If")?;
}
Ok(())
}
ast::Stmt::With(with_stmt) => {
let owner = if with_stmt.is_async {
"AsyncWith"
} else {
"With"
};
validate_nonempty_seq(vm, with_stmt.items.len(), "items", owner)?;
for item in &with_stmt.items {
validate_expr(vm, &item.context_expr, ast::ExprContext::Load)?;
if let Some(optional_vars) = &item.optional_vars {
validate_expr(vm, optional_vars, ast::ExprContext::Store)?;
}
}
validate_body(vm, &with_stmt.body, owner)
}
ast::Stmt::Match(match_stmt) => {
validate_expr(vm, &match_stmt.subject, ast::ExprContext::Load)?;
validate_nonempty_seq(vm, match_stmt.cases.len(), "cases", "Match")?;
for case in &match_stmt.cases {
validate_pattern(vm, &case.pattern, false)?;
if let Some(guard) = &case.guard {
validate_expr(vm, guard, ast::ExprContext::Load)?;
}
validate_body(vm, &case.body, "match_case")?;
}
Ok(())
}
ast::Stmt::Raise(raise) => {
if let Some(exc) = &raise.exc {
validate_expr(vm, exc, ast::ExprContext::Load)?;
if let Some(cause) = &raise.cause {
validate_expr(vm, cause, ast::ExprContext::Load)?;
}
} else if raise.cause.is_some() {
return Err(vm.new_value_error("Raise with cause but no exception"));
}
Ok(())
}
ast::Stmt::Try(try_stmt) => {
let owner = if try_stmt.is_star { "TryStar" } else { "Try" };
validate_body(vm, &try_stmt.body, owner)?;
if try_stmt.handlers.is_empty() && try_stmt.finalbody.is_empty() {
return Err(vm.new_value_error(format!(
"{owner} has neither except handlers nor finalbody"
)));
}
if try_stmt.handlers.is_empty() && !try_stmt.orelse.is_empty() {
return Err(
vm.new_value_error(format!("{owner} has orelse but no except handlers"))
);
}
for handler in &try_stmt.handlers {
let ast::ExceptHandler::ExceptHandler(handler) = handler;
if let Some(type_expr) = &handler.type_ {
validate_expr(vm, type_expr, ast::ExprContext::Load)?;
}
validate_body(vm, &handler.body, "ExceptHandler")?;
}
validate_stmts(vm, &try_stmt.finalbody)?;
validate_stmts(vm, &try_stmt.orelse)
}
ast::Stmt::Assert(assert_stmt) => {
validate_expr(vm, &assert_stmt.test, ast::ExprContext::Load)?;
if let Some(msg) = &assert_stmt.msg {
validate_expr(vm, msg, ast::ExprContext::Load)?;
}
Ok(())
}
ast::Stmt::Import(import) => {
validate_nonempty_seq(vm, import.names.len(), "names", "Import")?;
Ok(())
}
ast::Stmt::ImportFrom(import) => {
validate_nonempty_seq(vm, import.names.len(), "names", "ImportFrom")?;
Ok(())
}
ast::Stmt::Global(global) => {
validate_nonempty_seq(vm, global.names.len(), "names", "Global")?;
Ok(())
}
ast::Stmt::Nonlocal(nonlocal) => {
validate_nonempty_seq(vm, nonlocal.names.len(), "names", "Nonlocal")?;
Ok(())
}
ast::Stmt::Expr(expr) => validate_expr(vm, &expr.value, ast::ExprContext::Load),
ast::Stmt::Pass(_)
| ast::Stmt::Break(_)
| ast::Stmt::Continue(_)
| ast::Stmt::IpyEscapeCommand(_) => Ok(()),
}
}
fn validate_stmts(vm: &VirtualMachine, stmts: &[ast::Stmt]) -> PyResult<()> {
for stmt in stmts {
validate_stmt(vm, stmt)?;
}
Ok(())
}
pub(super) fn validate_mod(vm: &VirtualMachine, module: &Mod) -> PyResult<()> {
match module {
Mod::Module(module) => validate_stmts(vm, &module.body),
Mod::Interactive(module) => validate_stmts(vm, &module.body),
Mod::Expression(expr) => validate_expr(vm, &expr.body, ast::ExprContext::Load),
Mod::FunctionType(func_type) => {
validate_exprs(vm, &func_type.argtypes, ast::ExprContext::Load, false)?;
validate_expr(vm, &func_type.returns, ast::ExprContext::Load)
}
}
}