[syntax-errors] Store to or delete __debug__ (#16984)

Summary
--

Detect setting or deleting `__debug__`. Assigning to `__debug__` was a
`SyntaxError` on the earliest version I tested (3.8). Deleting
`__debug__` was made a `SyntaxError` in [BPO 45000], which said it was
resolved in Python 3.10. However, `del __debug__` was also a runtime
error (`NameError`) when I tested in Python 3.9.6, so I thought it was
worth including 3.9 in this check.

I don't think it was ever a *good* idea to try `del __debug__`, so I
think there's also an argument for not making this version-dependent at
all. That would only simplify the implementation very slightly, though.

[BPO 45000]: https://github.com/python/cpython/issues/89163

Test Plan
--

New inline tests. This also required adding a `PythonVersion` field to
the `TestContext` that could be taken from the inline `ParseOptions` and
making the version field on the options accessible.
This commit is contained in:
Brent Westbrook 2025-03-29 12:07:20 -04:00 committed by GitHub
parent 8396d7cd63
commit a0819f0c51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1372 additions and 7 deletions

View file

@ -9,7 +9,8 @@ use std::fmt::Display;
use ruff_python_ast::{
self as ast,
visitor::{walk_expr, Visitor},
Expr, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
Expr, ExprContext, IrrefutablePatternKind, Pattern, PythonVersion, Stmt, StmtExpr,
StmtImportFrom,
};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
@ -74,6 +75,105 @@ impl SemanticSyntaxChecker {
}
_ => {}
}
Self::debug_shadowing(stmt, ctx);
}
/// Check for [`SemanticSyntaxErrorKind::WriteToDebug`] in `stmt`.
fn debug_shadowing<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
type_params,
parameters,
..
}) => {
// test_err debug_shadow_function
// def __debug__(): ... # function name
// def f[__debug__](): ... # type parameter name
// def f(__debug__): ... # parameter name
Self::check_identifier(name, ctx);
if let Some(type_params) = type_params {
for type_param in type_params.iter() {
Self::check_identifier(type_param.name(), ctx);
}
}
for parameter in parameters {
Self::check_identifier(parameter.name(), ctx);
}
}
Stmt::ClassDef(ast::StmtClassDef {
name, type_params, ..
}) => {
// test_err debug_shadow_class
// class __debug__: ... # class name
// class C[__debug__]: ... # type parameter name
Self::check_identifier(name, ctx);
if let Some(type_params) = type_params {
for type_param in type_params.iter() {
Self::check_identifier(type_param.name(), ctx);
}
}
}
Stmt::TypeAlias(ast::StmtTypeAlias {
type_params: Some(type_params),
..
}) => {
// test_err debug_shadow_type_alias
// type __debug__ = list[int] # visited as an Expr but still flagged
// type Debug[__debug__] = str
for type_param in type_params.iter() {
Self::check_identifier(type_param.name(), ctx);
}
}
Stmt::Import(ast::StmtImport { names, .. })
| Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => {
// test_err debug_shadow_import
// import __debug__
// import debug as __debug__
// from x import __debug__
// from x import debug as __debug__
// test_ok debug_rename_import
// import __debug__ as debug
// from __debug__ import Some
// from x import __debug__ as debug
for name in names {
match &name.asname {
Some(asname) => Self::check_identifier(asname, ctx),
None => Self::check_identifier(&name.name, ctx),
}
}
}
Stmt::Try(ast::StmtTry { handlers, .. }) => {
// test_err debug_shadow_try
// try: ...
// except Exception as __debug__: ...
for handler in handlers
.iter()
.filter_map(ast::ExceptHandler::as_except_handler)
{
if let Some(name) = &handler.name {
Self::check_identifier(name, ctx);
}
}
}
// test_err debug_shadow_with
// with open("foo.txt") as __debug__: ...
_ => {}
}
}
/// Check if `ident` is equal to `__debug__` and emit a
/// [`SemanticSyntaxErrorKind::WriteToDebug`] if so.
fn check_identifier<Ctx: SemanticSyntaxContext>(ident: &ast::Identifier, ctx: &Ctx) {
if ident.id == "__debug__" {
Self::add_error(
ctx,
SemanticSyntaxErrorKind::WriteToDebug(WriteToDebugKind::Store),
ident.range,
);
}
}
fn duplicate_type_parameter_name<Ctx: SemanticSyntaxContext>(
@ -207,6 +307,51 @@ impl SemanticSyntaxChecker {
Self::check_generator_expr(key, generators, ctx);
Self::check_generator_expr(value, generators, ctx);
}
Expr::Name(ast::ExprName {
range,
id,
ctx: expr_ctx,
}) => {
// test_err write_to_debug_expr
// del __debug__
// del x, y, __debug__, z
// __debug__ = 1
// x, y, __debug__, z = 1, 2, 3, 4
// test_err del_debug_py39
// # parse_options: {"target-version": "3.9"}
// del __debug__
// test_ok del_debug_py38
// # parse_options: {"target-version": "3.8"}
// del __debug__
// test_ok read_from_debug
// if __debug__: ...
// x = __debug__
if id == "__debug__" {
match expr_ctx {
ExprContext::Store => Self::add_error(
ctx,
SemanticSyntaxErrorKind::WriteToDebug(WriteToDebugKind::Store),
*range,
),
ExprContext::Del => {
let version = ctx.python_version();
if version >= PythonVersion::PY39 {
Self::add_error(
ctx,
SemanticSyntaxErrorKind::WriteToDebug(
WriteToDebugKind::Delete(version),
),
*range,
);
}
}
_ => {}
};
}
}
_ => {}
}
}
@ -292,6 +437,12 @@ impl Display for SemanticSyntaxError {
f.write_str("wildcard makes remaining patterns unreachable")
}
},
SemanticSyntaxErrorKind::WriteToDebug(kind) => match kind {
WriteToDebugKind::Store => f.write_str("cannot assign to `__debug__`"),
WriteToDebugKind::Delete(python_version) => {
write!(f, "cannot delete `__debug__` on Python {python_version} (syntax was removed in 3.9)")
}
},
}
}
}
@ -370,6 +521,30 @@ pub enum SemanticSyntaxErrorKind {
///
/// [Python reference]: https://docs.python.org/3/reference/compound_stmts.html#irrefutable-case-blocks
IrrefutableCasePattern(IrrefutablePatternKind),
/// Represents a write to `__debug__`. This includes simple assignments and deletions as well
/// other kinds of statements that can introduce bindings, such as type parameters in functions,
/// classes, and aliases, `match` arms, and imports, among others.
///
/// ## Examples
///
/// ```python
/// del __debug__
/// __debug__ = False
/// def f(__debug__): ...
/// class C[__debug__]: ...
/// ```
///
/// See [BPO 45000] for more information.
///
/// [BPO 45000]: https://github.com/python/cpython/issues/89163
WriteToDebug(WriteToDebugKind),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WriteToDebugKind {
Store,
Delete(PythonVersion),
}
/// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in
@ -480,6 +655,10 @@ impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> {
ident.range(),
);
}
// test_err debug_shadow_match
// match x:
// case __debug__: ...
SemanticSyntaxChecker::check_identifier(ident, self.ctx);
}
}