[ty] Avoid enforcing standalone expression for tests in f-strings (#21967)

## Summary

Based on what we do elsewhere and my understanding of "standalone"
here...

Closes https://github.com/astral-sh/ty/issues/1865.
This commit is contained in:
Charlie Marsh 2025-12-15 22:31:04 -05:00 committed by GitHub
parent 8e13765b57
commit 682d29c256
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 13 deletions

View File

@ -152,6 +152,20 @@ The expressions in these string annotations aren't valid expressions in this con
shouldn't panic. shouldn't panic.
```py ```py
# Regression test for https://github.com/astral-sh/ty/issues/1865
# error: [fstring-type-annotation]
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
# error: [fstring-type-annotation]
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
# error: [fstring-type-annotation]
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
# error: [fstring-type-annotation]
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
# error: [fstring-type-annotation]
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
# error: [fstring-type-annotation]
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"
a: "1 or 2" a: "1 or 2"
b: "(x := 1)" b: "(x := 1)"
# error: [invalid-type-form] # error: [invalid-type-form]

View File

@ -522,6 +522,11 @@ impl<'db> SemanticIndex<'db> {
self.scopes_by_node[&node.node_key()] self.scopes_by_node[&node.node_key()]
} }
/// Returns the id of the scope that `node` creates, if it exists.
pub(crate) fn try_node_scope(&self, node: NodeWithScopeRef) -> Option<FileScopeId> {
self.scopes_by_node.get(&node.node_key()).copied()
}
/// Checks if there is an import of `__future__.annotations` in the global scope, which affects /// Checks if there is an import of `__future__.annotations` in the global scope, which affects
/// the logic for type inference. /// the logic for type inference.
pub(super) fn has_future_annotations(&self) -> bool { pub(super) fn has_future_annotations(&self) -> bool {

View File

@ -7926,7 +7926,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let Some(first_comprehension) = comprehensions_iter.next() else { let Some(first_comprehension) = comprehensions_iter.next() else {
unreachable!("Comprehension must contain at least one generator"); unreachable!("Comprehension must contain at least one generator");
}; };
self.infer_standalone_expression(&first_comprehension.iter, TypeContext::default()); self.infer_maybe_standalone_expression(&first_comprehension.iter, TypeContext::default());
if first_comprehension.is_async { if first_comprehension.is_async {
EvaluationMode::Async EvaluationMode::Async
@ -7946,9 +7946,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let evaluation_mode = self.infer_first_comprehension_iter(generators); let evaluation_mode = self.infer_first_comprehension_iter(generators);
let scope_id = self let Some(scope_id) = self
.index .index
.node_scope(NodeWithScopeRef::GeneratorExpression(generator)); .try_node_scope(NodeWithScopeRef::GeneratorExpression(generator))
else {
return Type::unknown();
};
let scope = scope_id.to_scope_id(self.db(), self.file()); let scope = scope_id.to_scope_id(self.db(), self.file());
let inference = infer_scope_types(self.db(), scope); let inference = infer_scope_types(self.db(), scope);
let yield_type = inference.expression_type(elt.as_ref()); let yield_type = inference.expression_type(elt.as_ref());
@ -8021,9 +8024,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_first_comprehension_iter(generators); self.infer_first_comprehension_iter(generators);
let scope_id = self let Some(scope_id) = self
.index .index
.node_scope(NodeWithScopeRef::ListComprehension(listcomp)); .try_node_scope(NodeWithScopeRef::ListComprehension(listcomp))
else {
return Type::unknown();
};
let scope = scope_id.to_scope_id(self.db(), self.file()); let scope = scope_id.to_scope_id(self.db(), self.file());
let inference = infer_scope_types(self.db(), scope); let inference = infer_scope_types(self.db(), scope);
let element_type = inference.expression_type(elt.as_ref()); let element_type = inference.expression_type(elt.as_ref());
@ -8046,9 +8052,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_first_comprehension_iter(generators); self.infer_first_comprehension_iter(generators);
let scope_id = self let Some(scope_id) = self
.index .index
.node_scope(NodeWithScopeRef::DictComprehension(dictcomp)); .try_node_scope(NodeWithScopeRef::DictComprehension(dictcomp))
else {
return Type::unknown();
};
let scope = scope_id.to_scope_id(self.db(), self.file()); let scope = scope_id.to_scope_id(self.db(), self.file());
let inference = infer_scope_types(self.db(), scope); let inference = infer_scope_types(self.db(), scope);
let key_type = inference.expression_type(key.as_ref()); let key_type = inference.expression_type(key.as_ref());
@ -8071,9 +8080,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_first_comprehension_iter(generators); self.infer_first_comprehension_iter(generators);
let scope_id = self let Some(scope_id) = self
.index .index
.node_scope(NodeWithScopeRef::SetComprehension(setcomp)); .try_node_scope(NodeWithScopeRef::SetComprehension(setcomp))
else {
return Type::unknown();
};
let scope = scope_id.to_scope_id(self.db(), self.file()); let scope = scope_id.to_scope_id(self.db(), self.file());
let inference = infer_scope_types(self.db(), scope); let inference = infer_scope_types(self.db(), scope);
let element_type = inference.expression_type(elt.as_ref()); let element_type = inference.expression_type(elt.as_ref());
@ -8165,14 +8177,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
builder.module(), builder.module(),
) )
} else { } else {
builder.infer_standalone_expression(iter, tcx) builder.infer_maybe_standalone_expression(iter, tcx)
} }
.iterate(builder.db()) .iterate(builder.db())
.homogeneous_element_type(builder.db()) .homogeneous_element_type(builder.db())
}); });
for expr in ifs { for expr in ifs {
self.infer_standalone_expression(expr, TypeContext::default()); self.infer_maybe_standalone_expression(expr, TypeContext::default());
} }
} }
@ -8278,7 +8290,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
orelse, orelse,
} = if_expression; } = if_expression;
let test_ty = self.infer_standalone_expression(test, TypeContext::default()); let test_ty = self.infer_maybe_standalone_expression(test, TypeContext::default());
let body_ty = self.infer_expression(body, tcx); let body_ty = self.infer_expression(body, tcx);
let orelse_ty = self.infer_expression(orelse, tcx); let orelse_ty = self.infer_expression(orelse, tcx);
@ -10341,7 +10353,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let ty = if index == values.len() - 1 { let ty = if index == values.len() - 1 {
builder.infer_expression(value, TypeContext::default()) builder.infer_expression(value, TypeContext::default())
} else { } else {
builder.infer_standalone_expression(value, TypeContext::default()) builder.infer_maybe_standalone_expression(value, TypeContext::default())
}; };
(ty, value.range()) (ty, value.range())