Skip to content

Commit 8fac42c

Browse files
committed
fold a typeof-bearing symbolic op nested in a subscript
1 parent 00bda33 commit 8fac42c

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

crates/by_transforms/src/transforms/callable.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ use crate::type_info::TypeInfo;
3737
pub(crate) struct CallableSyntax<'src> {
3838
source: &'src str,
3939
types: Option<&'src (dyn TypeInfo + 'src)>,
40+
/// ranges already folded by `symbolic_type_op` (e.g. `1 + typeof d` →
41+
/// `Literal[3]`). a claimed sub-expression is opaque here: descending into
42+
/// it would re-render its `typeof`/operator surface and the wider edit
43+
/// would clobber the fold, so `rewrite` leaves it for the fold's own edit
44+
claimed_ranges: &'src [TextRange],
4045
pub(crate) edits: Vec<Fix>,
4146
pub(crate) needs_import: bool,
4247
pub(crate) needs_protocol_import: bool,
@@ -64,6 +69,7 @@ impl<'src> CallableSyntax<'src> {
6469
Self {
6570
source,
6671
types: None,
72+
claimed_ranges: &[],
6773
edits: Vec::new(),
6874
needs_import: false,
6975
needs_protocol_import: false,
@@ -81,6 +87,11 @@ impl<'src> CallableSyntax<'src> {
8187
self
8288
}
8389

90+
pub(crate) fn with_claimed_ranges(mut self, claimed: &'src [TextRange]) -> Self {
91+
self.claimed_ranges = claimed;
92+
self
93+
}
94+
8495
pub(crate) fn class_defs(&self) -> &str {
8596
&self.protocol_class_defs
8697
}
@@ -228,6 +239,12 @@ impl<'src> CallableSyntax<'src> {
228239
}
229240

230241
pub(crate) fn rewrite(&mut self, expr: &Expr) -> Option<String> {
242+
// a symbolic-fold-claimed sub-expression is opaque: the fold emits its
243+
// own edit over this exact range, so re-rendering here (and clobbering
244+
// it with a wider edit) must not happen
245+
if self.claimed_ranges.contains(&expr.range()) {
246+
return None;
247+
}
231248
match expr {
232249
Expr::CallableType(ct) if self.is_non_denotable(ct) => {
233250
self.needs_protocol_import = true;
@@ -517,7 +534,12 @@ impl<'ast> ruff_python_ast::visitor::Visitor<'ast> for ValueCallableWalker<'_, '
517534

518535
impl TypeAwarePass for CallableSyntaxPass<'_> {
519536
fn run(&self, stmts: &[Stmt], types: &dyn TypeInfo, ctx: &mut PassContext) {
520-
let mut inner = CallableSyntax::new(self.source).with_types(types);
537+
// owned copy so `inner`'s borrow doesn't pin `ctx` against the mutable
538+
// `required_imports` / `edits` uses below
539+
let claimed = ctx.claimed_type_op_ranges.clone();
540+
let mut inner = CallableSyntax::new(self.source)
541+
.with_types(types)
542+
.with_claimed_ranges(&claimed);
521543
crate::transforms::type_expr_walker::walk_type_positions_skipping(
522544
stmts,
523545
Some(types),

crates/by_transforms/src/transforms/symbolic_type_op.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,20 @@ mod tests {
415415
);
416416
}
417417

418+
#[test]
419+
fn typeof_operand_nested_in_subscript() {
420+
// the fold consumes the `typeof` operand even inside a subscript slice,
421+
// so no `TypeOf` survives and the whole operation collapses to its type
422+
check(
423+
"let d = 2\nx: list[1 + typeof d]\n",
424+
indoc! {"
425+
from typing import Final, Literal
426+
d: Final = 2
427+
x: list[Literal[3]]
428+
"},
429+
);
430+
}
431+
418432
#[test]
419433
fn chained_addition() {
420434
check(

crates/ty_python_semantic/resources/mdtest/basedpython_symbolic_type_ops.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ def f(e: 1 + typeof d) -> None:
4343
reveal_type(e) # revealed: 3
4444
```
4545

46+
## a `typeof` operand folds inside a subscript too
47+
48+
the fold reaches operations nested in a generic subscript — the whole operation collapses to its
49+
type and the `typeof` is consumed, so no `TypeOf` leaks into the slice
50+
51+
```by
52+
let d = 2
53+
54+
def f(xs: list[1 + typeof d]) -> None:
55+
reveal_type(xs[0]) # revealed: 3
56+
```
57+
4658
## the user's full example
4759

4860
```by

0 commit comments

Comments
 (0)