Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,14 +551,16 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
| SemanticSyntaxErrorKind::WriteToDebug(_)
if self.settings.preview.is_enabled() =>
{
self.semantic_errors.borrow_mut().push(error);
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_) => {}
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
| SemanticSyntaxErrorKind::WriteToDebug(_) => {}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class __debug__: ... # class name
class C[__debug__]: ... # type parameter name
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def __debug__(): ... # function name
def f[__debug__](): ... # type parameter name
def f(__debug__): ... # parameter name
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import __debug__
import debug as __debug__
from x import __debug__
from x import debug as __debug__
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
match x:
case __debug__: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
try: ...
except Exception as __debug__: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type __debug__ = list[int] # visited as an Expr but still flagged
type Debug[__debug__] = str
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
with open("foo.txt") as __debug__: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# parse_options: {"target-version": "3.9"}
del __debug__
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
del __debug__
del x, y, __debug__, z
__debug__ = 1
x, y, __debug__, z = 1, 2, 3, 4
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import __debug__ as debug
from __debug__ import Some
from x import __debug__ as debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# parse_options: {"target-version": "3.8"}
del __debug__
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
if __debug__: ...
x = __debug__
4 changes: 4 additions & 0 deletions crates/ruff_python_parser/src/parser/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ impl ParseOptions {
self.target_version = target_version;
self
}

pub fn target_version(&self) -> PythonVersion {
self.target_version
}
}

impl From<Mode> for ParseOptions {
Expand Down
181 changes: 180 additions & 1 deletion crates/ruff_python_parser/src/semantic_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are more cases that need handling where we don't use an ExprName in the AST

from x import __debug__
import __debug__

match x:
	__debug__: ...

f(__debug__ = b)

Finding the Identifier nodes is somewhat annoying because our visitor doesn't traverse Identifier nodes. You can take a look at where I believe I identified all Identifier nodes

pub(crate) enum GoToTarget<'a> {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch. I think I should have covered all of these now.

For match patterns, I moved the check into the MultipleCaseAssignmentVisitor, so it might be worth renaming that to something more generic like MatchPatternVisitor or just PatternVisitor now too.

// 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,
);
}
}
_ => {}
};
}
}
_ => {}
}
}
Expand Down Expand Up @@ -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)")
}
},
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

Expand Down
23 changes: 18 additions & 5 deletions crates/ruff_python_parser/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn test_valid_syntax(input_path: &Path) {
let options = extract_options(&source).unwrap_or_else(|| {
ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest())
});
let parsed = parse_unchecked(&source, options);
let parsed = parse_unchecked(&source, options.clone());

if parsed.has_syntax_errors() {
let line_index = LineIndex::from_source_text(&source);
Expand Down Expand Up @@ -88,7 +88,9 @@ fn test_valid_syntax(input_path: &Path) {

let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");

let mut visitor = SemanticSyntaxCheckerVisitor::new(TestContext::default());
let mut visitor = SemanticSyntaxCheckerVisitor::new(
TestContext::default().with_python_version(options.target_version()),
);

for stmt in parsed.suite() {
visitor.visit_stmt(stmt);
Expand Down Expand Up @@ -134,7 +136,7 @@ fn test_invalid_syntax(input_path: &Path) {
let options = extract_options(&source).unwrap_or_else(|| {
ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest())
});
let parsed = parse_unchecked(&source, options);
let parsed = parse_unchecked(&source, options.clone());

validate_tokens(parsed.tokens(), source.text_len(), input_path);
validate_ast(parsed.syntax(), source.text_len(), input_path);
Expand Down Expand Up @@ -182,7 +184,9 @@ fn test_invalid_syntax(input_path: &Path) {

let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");

let mut visitor = SemanticSyntaxCheckerVisitor::new(TestContext::default());
let mut visitor = SemanticSyntaxCheckerVisitor::new(
TestContext::default().with_python_version(options.target_version()),
);

for stmt in parsed.suite() {
visitor.visit_stmt(stmt);
Expand Down Expand Up @@ -461,6 +465,15 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
#[derive(Debug, Default)]
struct TestContext {
diagnostics: RefCell<Vec<SemanticSyntaxError>>,
python_version: PythonVersion,
}

impl TestContext {
#[must_use]
fn with_python_version(mut self, python_version: PythonVersion) -> Self {
self.python_version = python_version;
self
}
}

impl SemanticSyntaxContext for TestContext {
Expand All @@ -469,7 +482,7 @@ impl SemanticSyntaxContext for TestContext {
}

fn python_version(&self) -> PythonVersion {
PythonVersion::default()
self.python_version
}

fn report_semantic_error(&self, error: SemanticSyntaxError) {
Expand Down
Loading
Loading