Skip to content

Commit c6f071e

Browse files
committed
fix three checker/runtime divergences the harness surfaced
1 parent 28239bb commit c6f071e

6 files changed

Lines changed: 118 additions & 41 deletions

File tree

crates/by_transforms/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,12 @@ fn run_lowering_phase(
508508
_types: &dyn TypeInfo,
509509
) -> LoweringResult {
510510
let mut output = String::new();
511-
if config.inject_future_annotations && !config.is_stub && !has_future_annotations(stmts) {
511+
// below 3.10 the runtime cannot evaluate pep 604 `X | Y` annotations —
512+
// which the optional lowering itself produces — so annotation evaluation
513+
// must always be deferred on those targets
514+
let needs_lazy_annotations =
515+
config.inject_future_annotations || config.min_version < PythonVersion::PY310;
516+
if needs_lazy_annotations && !config.is_stub && !has_future_annotations(stmts) {
512517
output.push_str("from __future__ import annotations\n");
513518
}
514519
output.push_str(source);

crates/by_transforms/src/transforms/ast_driver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ pub(crate) fn run_against_source<'a>(
416416
text_edits: RefCell::new(vec![]),
417417
};
418418

419-
let typed_lambda_inner = typed_lambda::TypedLambda::new();
419+
let typed_lambda_inner = typed_lambda::TypedLambda::new(source_ref);
420420
let typed_lambda_pass = VisitorPass {
421421
inner: &typed_lambda_inner,
422422
changed_cell: typed_lambda_inner.changed_cell(),

crates/by_transforms/src/transforms/auto_quote.rs

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ impl<'src> AutoQuote<'src> {
4545

4646
impl AstPass for AutoQuote<'_> {
4747
fn run(&self, module: &mut ModModule, ctx: &mut PassContext) {
48-
// skip quoting whenever annotations won't be eagerly evaluated:
49-
// native deferral on 3.14+, or a future import that defers them all
50-
if self.min_version.defers_annotations()
48+
// annotation positions need no quoting when they won't be eagerly
49+
// evaluated: native deferral on 3.14+ (PEP 649), or a future import
50+
// that defers them all. class *bases* and value-position subscripts
51+
// (`class A(list[A])`, `list[A]()`) evaluate eagerly regardless, so
52+
// their self-references are always quoted
53+
let quote_annotations = !(self.min_version.defers_annotations()
5154
|| self.inject_future
52-
|| has_future_annotations(&module.body)
53-
{
54-
return;
55-
}
55+
|| has_future_annotations(&module.body));
5656
let mut edits: Vec<(TextRange, String)> = Vec::new();
57-
process_stmts(&module.body, self.source, &mut edits);
57+
process_stmts(&module.body, self.source, &mut edits, quote_annotations);
5858
ctx.text_edits.extend(edits);
5959
}
6060
}
@@ -67,51 +67,66 @@ fn has_future_annotations(stmts: &[Stmt]) -> bool {
6767
})
6868
}
6969

70-
fn process_stmts(stmts: &[Stmt], source: &str, edits: &mut Vec<(TextRange, String)>) {
70+
fn process_stmts(
71+
stmts: &[Stmt],
72+
source: &str,
73+
edits: &mut Vec<(TextRange, String)>,
74+
quote_annotations: bool,
75+
) {
7176
for stmt in stmts {
7277
if let Stmt::ClassDef(c) = stmt {
73-
process_class(c, source, edits);
78+
process_class(c, source, edits, quote_annotations);
7479
// nested classes inside this one's body are recursed into by
7580
// process_class so the inner ClassDef walks with its own name
7681
} else {
7782
// top-level non-class statements may contain nested classes via
7883
// function bodies — descend
79-
walk_for_nested_classes(stmt, source, edits);
84+
walk_for_nested_classes(stmt, source, edits, quote_annotations);
8085
}
8186
}
8287
}
8388

84-
fn walk_for_nested_classes(stmt: &Stmt, source: &str, edits: &mut Vec<(TextRange, String)>) {
89+
fn walk_for_nested_classes(
90+
stmt: &Stmt,
91+
source: &str,
92+
edits: &mut Vec<(TextRange, String)>,
93+
quote_annotations: bool,
94+
) {
8595
// only walk into structures that may contain nested class defs.
8696
// function bodies, if/while/try blocks, etc.
8797
match stmt {
88-
Stmt::FunctionDef(f) => process_stmts(&f.body, source, edits),
98+
Stmt::FunctionDef(f) => process_stmts(&f.body, source, edits, quote_annotations),
8999
Stmt::If(i) => {
90-
process_stmts(&i.body, source, edits);
100+
process_stmts(&i.body, source, edits, quote_annotations);
91101
for clause in &i.elif_else_clauses {
92-
process_stmts(&clause.body, source, edits);
102+
process_stmts(&clause.body, source, edits, quote_annotations);
93103
}
94104
}
95-
Stmt::While(w) => process_stmts(&w.body, source, edits),
105+
Stmt::While(w) => process_stmts(&w.body, source, edits, quote_annotations),
96106
Stmt::For(f) => {
97-
process_stmts(&f.body, source, edits);
98-
process_stmts(&f.orelse, source, edits);
107+
process_stmts(&f.body, source, edits, quote_annotations);
108+
process_stmts(&f.orelse, source, edits, quote_annotations);
99109
}
100-
Stmt::With(w) => process_stmts(&w.body, source, edits),
110+
Stmt::With(w) => process_stmts(&w.body, source, edits, quote_annotations),
101111
Stmt::Try(t) => {
102-
process_stmts(&t.body, source, edits);
112+
process_stmts(&t.body, source, edits, quote_annotations);
103113
for h in &t.handlers {
104114
let ruff_python_ast::ExceptHandler::ExceptHandler(eh) = h;
105-
process_stmts(&eh.body, source, edits);
115+
process_stmts(&eh.body, source, edits, quote_annotations);
106116
}
107-
process_stmts(&t.orelse, source, edits);
108-
process_stmts(&t.finalbody, source, edits);
117+
process_stmts(&t.orelse, source, edits, quote_annotations);
118+
process_stmts(&t.finalbody, source, edits, quote_annotations);
109119
}
110120
_ => {}
111121
}
112122
}
113123

114-
fn process_class(class: &StmtClassDef, source: &str, edits: &mut Vec<(TextRange, String)>) {
124+
fn process_class(
125+
class: &StmtClassDef,
126+
source: &str,
127+
edits: &mut Vec<(TextRange, String)>,
128+
quote_annotations: bool,
129+
) {
115130
let class_name = class.name.id.as_str();
116131
let typevar_names: Vec<String> = class
117132
.type_params
@@ -148,25 +163,30 @@ fn process_class(class: &StmtClassDef, source: &str, edits: &mut Vec<(TextRange,
148163
// (handled by walker), method bodies need separate descent for
149164
// `list[A]()` patterns
150165
for stmt in &class.body {
151-
process_class_body_stmt(stmt, &mut visitor);
166+
process_class_body_stmt(stmt, &mut visitor, quote_annotations);
152167
}
153168

154169
// recurse into nested classes inside the body so they get their own
155170
// class-context walk (the `visitor` borrow of `edits` ends above)
156-
process_stmts(&class.body, source, edits);
171+
process_stmts(&class.body, source, edits, quote_annotations);
157172
}
158173

159-
fn process_class_body_stmt(stmt: &Stmt, visitor: &mut Visitor<'_>) {
174+
fn process_class_body_stmt(stmt: &Stmt, visitor: &mut Visitor<'_>, quote_annotations: bool) {
160175
match stmt {
161176
Stmt::Expr(e) => walk_value_subscripts(e.value.as_ref(), visitor),
162177
Stmt::Assign(a) => walk_value_subscripts(a.value.as_ref(), visitor),
163178
Stmt::AnnAssign(a) => {
164-
walk_one_type_expr(a.annotation.as_ref(), visitor);
179+
if quote_annotations {
180+
walk_one_type_expr(a.annotation.as_ref(), visitor);
181+
}
165182
if let Some(value) = &a.value {
166183
walk_value_subscripts(value.as_ref(), visitor);
167184
}
168185
}
169186
Stmt::FunctionDef(f) => {
187+
if !quote_annotations {
188+
return;
189+
}
170190
for param in f.parameters.iter_non_variadic_params() {
171191
if let Some(ann) = &param.parameter.annotation {
172192
walk_one_type_expr(ann, visitor);
@@ -516,6 +536,21 @@ mod tests {
516536
);
517537
}
518538

539+
#[test]
540+
fn class_base_still_quoted_when_version_defers_annotations() {
541+
// pep 649 defers annotations only — a class *base* evaluates eagerly,
542+
// so its self-reference still needs the quote
543+
let config = Config {
544+
min_version: PythonVersion::from((3, 14)),
545+
..Config::test_default()
546+
};
547+
let out = transpile_with("class A(list[A]):\n pass\n", &config);
548+
assert!(
549+
out.contains("class A(list[\"A\"]):"),
550+
"base self-ref must stay quoted on 3.14+, got: {out}"
551+
);
552+
}
553+
519554
#[test]
520555
fn not_quoted_when_source_has_future() {
521556
// a user-written future import already defers every annotation

crates/by_transforms/src/transforms/mutable_defaults.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
//! function, body included, keeps its source bytes, so sibling lowerings
1515
//! (`??`, `?.`, `int?` annotations, …) anywhere in the function still apply.
1616
17+
use std::fmt::Write as _;
18+
1719
use ruff_python_ast::visitor::{Visitor, walk_stmt};
1820
use ruff_python_ast::{Expr, Stmt, StmtFunctionDef, UnaryOp};
1921
use ruff_text_size::{Ranged, TextRange};
@@ -92,30 +94,33 @@ impl MutableDefaults<'_> {
9294
// each guard re-establishes it for the following line
9395
let base = prefix.to_owned();
9496
for (name, default) in &guards {
95-
text.push_str(&format!(
97+
let _ = write!(
98+
text,
9699
"if {name} is _MISSING:\n{base} {name} = {default}\n{base}"
97-
));
100+
);
98101
}
99102
} else {
100103
// single-line body (`def f(x=[]): ...`) — break it onto its own
101104
// indented line after the guards
102105
let base = format!("{} ", line_indent(self.source, f.range().start()));
103106
for (name, default) in &guards {
104-
text.push_str(&format!(
107+
let _ = write!(
108+
text,
105109
"\n{base}if {name} is _MISSING:\n{base} {name} = {default}"
106-
));
110+
);
107111
}
108-
text.push_str(&format!("\n{base}"));
112+
let _ = write!(text, "\n{base}");
109113
}
110114
self.edits.push((TextRange::empty(insert_at), text));
111115
} else {
112116
// docstring-only body: append the guards after it
113117
let doc_end = f.body[docstring_count - 1].range().end();
114118
let base = format!("{} ", line_indent(self.source, f.range().start()));
115119
for (name, default) in &guards {
116-
text.push_str(&format!(
120+
let _ = write!(
121+
text,
117122
"\n{base}if {name} is _MISSING:\n{base} {name} = {default}"
118-
));
123+
);
119124
}
120125
self.edits.push((TextRange::empty(doc_end), text));
121126
}

crates/by_transforms/src/transforms/optional_type.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ class Optional:
196196
check("x: int?\n", "x: int | None\n");
197197
}
198198

199+
#[test]
200+
fn py39_target_defers_annotation_evaluation() {
201+
// below 3.10 the runtime cannot evaluate the pep 604 union this very
202+
// lowering produces, so the future import is mandatory
203+
let config = crate::Config {
204+
min_version: crate::PythonVersion::PY39,
205+
..crate::Config::test_default()
206+
};
207+
assert_eq!(
208+
crate::transpile("x: int? = None\n", &config).unwrap(),
209+
"from __future__ import annotations\nx: int | None = None\n"
210+
);
211+
}
212+
199213
#[test]
200214
fn optional_of_negation_composes() {
201215
// the operand source is preserved, so the `not_type` transform still

crates/by_transforms/src/transforms/typed_lambda.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ use std::cell::Cell;
1212

1313
use ruff_python_ast::visitor::transformer::{Transformer, walk_expr};
1414
use ruff_python_ast::{Expr, Parameters, Stmt};
15+
use ruff_text_size::{Ranged, TextRange};
1516

16-
pub(crate) struct TypedLambda {
17+
pub(crate) struct TypedLambda<'src> {
18+
source: &'src str,
1719
changed: Cell<bool>,
1820
}
1921

20-
impl TypedLambda {
21-
pub(crate) fn new() -> Self {
22+
impl<'src> TypedLambda<'src> {
23+
pub(crate) fn new(source: &'src str) -> Self {
2224
Self {
25+
source,
2326
changed: Cell::new(false),
2427
}
2528
}
@@ -54,7 +57,7 @@ impl TypedLambda {
5457
}
5558
}
5659

57-
impl Transformer for TypedLambda {
60+
impl Transformer for TypedLambda<'_> {
5861
fn visit_stmt(&self, stmt: &mut Stmt) {
5962
ruff_python_ast::visitor::transformer::walk_stmt(self, stmt);
6063
}
@@ -73,6 +76,14 @@ impl Transformer for TypedLambda {
7376
lambda.returns = None;
7477
any = true;
7578
}
79+
// a parenthesized parameter list (`lambda (x): …`) is based-only
80+
// surface even without annotations — re-render to the bare form
81+
if let Some(p) = lambda.parameters.as_deref()
82+
&& p.range() != TextRange::default()
83+
&& self.source.as_bytes().get(usize::from(p.range().start())) == Some(&b'(')
84+
{
85+
any = true;
86+
}
7687
if any {
7788
self.changed.set(true);
7889
}
@@ -109,6 +120,13 @@ mod tests {
109120
check("a = lambda () -> int: 42\n", "a = lambda : 42\n");
110121
}
111122

123+
#[test]
124+
fn parenthesized_untyped_lambda() {
125+
// parens around lambda params are based-only surface even with no
126+
// annotations — they must not leak into the output
127+
check("g = lambda (x): str(x)\n", "g = lambda x: str(x)\n");
128+
}
129+
112130
#[test]
113131
fn untyped_lambda_unchanged() {
114132
check("a = lambda x, y: x + y\n", "a = lambda x, y: x + y\n");

0 commit comments

Comments
 (0)