blob: 134f02458001ac20c05c645ff5473764616b8e3a [file] [edit]
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
use pyrefly_graph::index::Idx;
use pyrefly_python::ast::Ast;
use pyrefly_python::module_path::ModuleStyle;
use pyrefly_python::short_identifier::ShortIdentifier;
use pyrefly_util::visit::VisitMut;
use ruff_python_ast::AtomicNodeIndex;
use ruff_python_ast::BoolOp;
use ruff_python_ast::Comprehension;
use ruff_python_ast::Decorator;
use ruff_python_ast::Expr;
use ruff_python_ast::ExprAttribute;
use ruff_python_ast::ExprBinOp;
use ruff_python_ast::ExprBoolOp;
use ruff_python_ast::ExprCall;
use ruff_python_ast::ExprLambda;
use ruff_python_ast::ExprName;
use ruff_python_ast::ExprNoneLiteral;
use ruff_python_ast::ExprStringLiteral;
use ruff_python_ast::ExprSubscript;
use ruff_python_ast::ExprYield;
use ruff_python_ast::ExprYieldFrom;
use ruff_python_ast::Identifier;
use ruff_python_ast::Operator;
use ruff_python_ast::StringLiteral;
use ruff_python_ast::name::Name;
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
use starlark_map::Hashed;
use thin_vec::ThinVec;
use vec1::Vec1;
use crate::binding::binding::Binding;
use crate::binding::binding::BindingDecorator;
use crate::binding::binding::BindingExpect;
use crate::binding::binding::BindingYield;
use crate::binding::binding::BindingYieldFrom;
use crate::binding::binding::ClassBodyUnknownName;
use crate::binding::binding::IsAsync;
use crate::binding::binding::Key;
use crate::binding::binding::KeyDecorator;
use crate::binding::binding::KeyExpect;
use crate::binding::binding::KeyYield;
use crate::binding::binding::KeyYieldFrom;
use crate::binding::binding::LambdaKind;
use crate::binding::binding::LinkedKey;
use crate::binding::binding::NarrowUseLocation;
use crate::binding::binding::PrivateAttributeAccessCheck;
use crate::binding::binding::SuperStyle;
use crate::binding::bindings::AwaitContext;
use crate::binding::bindings::BindingsBuilder;
use crate::binding::bindings::LegacyTParamCollector;
use crate::binding::bindings::LegacyTParamId;
use crate::binding::bindings::NameLookupResult;
use crate::binding::narrow::AtomicNarrowOp;
use crate::binding::narrow::NarrowOps;
use crate::binding::narrow::NarrowSource;
use crate::binding::scope::FlowStyle;
use crate::binding::scope::Scope;
use crate::binding::scope::TerminationKind;
use crate::binding::scope::is_constant_name;
use crate::config::error_kind::ErrorKind;
use crate::export::special::SpecialExport;
use crate::types::callable::unexpected_keyword;
use crate::types::types::AnyStyle;
/// Match on an expression by name. Should be used only for special names that we essentially treat like keywords,
/// like reveal_type.
fn is_special_name(name: &str) -> bool {
matches!(name, "reveal_type" | "assert_type")
}
/// Walk a chain of `Expr::Attribute` nodes (e.g. `a.b.c`) and collect the
/// base `ExprName` and attribute identifiers in order. Returns `None` if the
/// chain doesn't bottom out in a Name.
fn chase_static_attr_chain(mut expr: &Expr) -> Option<(ExprName, Vec1<Identifier>)> {
let mut attrs = Vec::new();
loop {
match expr {
Expr::Attribute(ExprAttribute { value, attr, .. }) => {
attrs.push(attr.clone());
expr = value;
}
Expr::Name(name) => {
attrs.reverse();
return Some((name.clone(), Vec1::try_from_vec(attrs).ok()?));
}
_ => return None,
}
}
}
/// Looking up names in an expression requires knowing the identity of the binding
/// we are computing for usage tracking.
///
/// There are some cases - particularly in type declaration contexts like annotations,
/// type variable declarations, and match patterns - that we want to skip for usage
/// tracking.
#[derive(Debug, Clone)]
pub enum Usage {
/// Normal usage context that may pin partial types.
/// The idx is the current binding being computed.
CurrentIdx(Idx<Key>),
/// Value context that should not pin partial types.
/// The idx (if present) is used for secondary-read detection.
NonPinningValue(Option<Idx<Key>>),
/// Static type context that should not pin partial types.
/// When `is_annotation` is true, implicit alias validation is applied.
StaticTypeInformation { is_annotation: bool },
/// Type alias RHS context. Like StaticTypeInformation, does not pin
/// partial types. Additionally signals that names resolving to type
/// alias bindings should produce Binding::TypeAliasRef instead of
/// Binding::Forward.
TypeAliasRhs,
}
impl Usage {
/// Create a non-pinning value usage from another usage context.
pub fn non_pinning_value_from(other: &Self) -> Self {
match other {
Self::CurrentIdx(idx) => Self::NonPinningValue(Some(*idx)),
Self::NonPinningValue(idx) => Self::NonPinningValue(*idx),
Self::StaticTypeInformation { .. } | Self::TypeAliasRhs => Self::NonPinningValue(None),
}
}
/// Get the current binding idx, if any.
pub fn current_idx(&self) -> Option<Idx<Key>> {
match self {
Usage::CurrentIdx(idx) => Some(*idx),
Usage::NonPinningValue(idx) => *idx,
Usage::StaticTypeInformation { .. } | Usage::TypeAliasRhs => None,
}
}
/// Whether this usage context may pin partial types.
pub fn may_pin_partial_type(&self) -> bool {
matches!(self, Usage::CurrentIdx(_))
}
/// Whether this usage is in a static type.
pub fn is_static(&self) -> bool {
matches!(
self,
Usage::StaticTypeInformation { .. } | Usage::TypeAliasRhs
)
}
}
enum TestAssertion {
AssertTrue,
AssertFalse,
AssertIsNone,
AssertIsNotNone,
AssertIsInstance,
AssertNotIsInstance,
AssertIs,
AssertIsNot,
AssertEqual,
AssertNotEqual,
AssertIn,
AssertNotIn,
}
impl TestAssertion {
pub fn to_narrow_ops(&self, builder: &BindingsBuilder, args: &[Expr]) -> Option<NarrowOps> {
match self {
Self::AssertTrue if let Some(arg0) = args.first() => {
Some(NarrowOps::from_expr(builder, Some(arg0)))
}
Self::AssertFalse if let Some(arg0) = args.first() => {
Some(NarrowOps::from_expr(builder, Some(arg0)).negate())
}
Self::AssertIsNone if let Some(arg0) = args.first() => {
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::Is(Expr::NoneLiteral(ExprNoneLiteral {
node_index: AtomicNodeIndex::default(),
range: TextRange::default(),
})),
arg0.range(),
))
}
Self::AssertIsNotNone if let Some(arg0) = args.first() => {
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::IsNot(Expr::NoneLiteral(ExprNoneLiteral {
node_index: AtomicNodeIndex::default(),
range: TextRange::default(),
})),
arg0.range(),
))
}
Self::AssertIsInstance
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::IsInstance(arg1.clone(), NarrowSource::Call),
arg0.range(),
))
}
Self::AssertNotIsInstance
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::IsNotInstance(arg1.clone(), NarrowSource::Call),
arg0.range(),
))
}
Self::AssertEqual
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::Eq(arg1.clone()),
arg0.range(),
))
}
Self::AssertNotEqual
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::NotEq(arg1.clone()),
arg0.range(),
))
}
Self::AssertIs
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::Is(arg1.clone()),
arg0.range(),
))
}
Self::AssertIsNot
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::IsNot(arg1.clone()),
arg0.range(),
))
}
Self::AssertIn
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::In(arg1.clone()),
arg0.range(),
))
}
Self::AssertNotIn
if let Some(arg0) = args.first()
&& let Some(arg1) = args.get(1) =>
{
Some(NarrowOps::from_single_narrow_op(
arg0,
AtomicNarrowOp::NotIn(arg1.clone()),
arg0.range(),
))
}
_ => None,
}
}
}
impl<'a> BindingsBuilder<'a> {
/// Ensure the name in an `ExprName`. Note that unlike `ensure_expr`, it
/// does not require a mutable ref.
pub fn ensure_expr_name(&mut self, x: &ExprName, usage: &mut Usage) -> Idx<Key> {
let name = Ast::expr_name_identifier(x.clone());
self.ensure_name(&name, usage, None)
}
fn ensure_name(
&mut self,
name: &Identifier,
usage: &mut Usage,
tparams_builder: Option<&mut LegacyTParamCollector>,
) -> Idx<Key> {
self.ensure_name_in_type(name, usage, tparams_builder, false, false)
}
fn ensure_name_in_type(
&mut self,
name: &Identifier,
usage: &mut Usage,
tparams_builder: Option<&mut LegacyTParamCollector>,
is_runtime_evaluated_annotation: bool,
allow_class_body_forward_reference: bool,
) -> Idx<Key> {
self.ensure_name_impl(
name,
usage,
tparams_builder
.map(|tparams_builder| (tparams_builder, LegacyTParamId::Name(name.clone()))),
is_runtime_evaluated_annotation,
allow_class_body_forward_reference,
)
}
fn ensure_simple_attr(
&mut self,
value: &Identifier,
attrs: Vec1<Identifier>,
usage: &mut Usage,
tparams_builder: Option<&mut LegacyTParamCollector>,
) -> Idx<Key> {
self.ensure_name_impl(
value,
usage,
tparams_builder.map(|tparams_builder| {
(tparams_builder, LegacyTParamId::Attr(value.clone(), attrs))
}),
false,
false,
)
}
/// Given a name appearing in an expression, create a `Usage` key for that
/// name at the current location. The binding will indicate how to compute
/// the type if we found that name in scope; if we do not find the name we
/// record an error and fall back to `Any`.
///
/// This function is the core scope lookup logic for binding creation.
///
/// To do the ensure, we need:
/// - Information about what binding it is being used in, which is used both
/// - to track first-use to get deterministic inference of placeholder
/// types like empty list
/// - to determine when we are in a static typing usage
/// - The lookup kind, which is used to distinguish between normal lookups,
/// which allow uses of nonlocals, versus mutable lookups that do not
/// (unless the nonlocal was explicitly mutably captured by a `global`
/// or `nonlocal` statement).
/// - An optional `tparams_lookup`, which intercepts names - but only
/// in static type contexts - that map to legacy type variables. It
/// is a flexible callback in order to handle not only bare name type
/// variables, but also `<module>.<name>` type variables, which have
/// to be modeled as attribute narrows of the module at solve time.
fn ensure_name_impl(
&mut self,
name: &Identifier,
usage: &mut Usage,
tparams_lookup: Option<(&mut LegacyTParamCollector, LegacyTParamId)>,
is_runtime_evaluated_annotation: bool,
allow_class_body_forward_reference: bool,
) -> Idx<Key> {
let key = Key::BoundName(ShortIdentifier::new(name));
if name.is_empty() {
// We only get empty identifiers if Ruff has done error correction,
// so there must be a parse error.
//
// Occasionally Ruff might give out the same Identifier twice in an error.
//
// We still need to produce a `Key` here just to be safe, because other
// code may rely on all `Identifier`s having `Usage` keys and we could panic
// in an IDE setting if we don't ensure this is the case.
return self.insert_binding_overwrite(key, Binding::Any(AnyStyle::Error));
}
let lookup_result = if usage.is_static()
&& let Some((tparams_collector, tparam_id)) = tparams_lookup
{
self.intercept_lookup(tparams_collector, tparam_id)
} else {
self.lookup_name(Hashed::new(&name.id), usage)
};
match lookup_result {
NameLookupResult::Found {
idx: lookup_result_idx,
initialized: is_initialized,
is_module_scope,
is_outer_class_type_parameter,
} => {
if is_outer_class_type_parameter {
return self.insert_binding(
key,
Binding::OuterClassTypeParameter(lookup_result_idx, name.range),
);
}
// Uninitialized local errors are only reported when we are neither in a stub
// nor a static type context.
if !usage.is_static() && !self.module_info.path().is_interface() {
if let Some(termination_keys) = is_initialized
.deferred_termination_keys()
.map(|s| s.to_vec())
{
// Defer the uninitialized check to solve time.
// At solve time, we'll check if all termination keys have Never type.
self.insert_binding(
KeyExpect::UninitializedCheck(name.range),
BindingExpect::UninitializedCheck {
name: name.id.clone(),
range: name.range,
termination_keys,
},
);
} else if let Some(error_message) = is_initialized.as_error_message(&name.id) {
self.error(name.range, ErrorKind::UnboundName, error_message);
}
}
if is_runtime_evaluated_annotation
&& matches!(
usage,
Usage::StaticTypeInformation {
is_annotation: true
}
)
&& self.module_info.path().style() == ModuleStyle::Executable
&& !self.sys_info.version().at_least(3, 14)
&& !self.scopes.has_future_annotations()
&& let Some(error_message) = is_initialized.as_error_message(&name.id)
{
self.error(name.range, ErrorKind::UnboundName, error_message);
}
// TODO: `global x` reads bypass this (they use Flow, not Anywhere).
let promote = self.scopes.in_function_scope()
&& (is_module_scope || self.scopes.is_defined_at_module_scope(&name.id))
&& !is_constant_name(&name.id)
&& !self.scopes.is_final_at_module_scope(&name.id);
if promote {
self.promote_ranges.insert(name.range);
}
self.defer_bound_name(key, lookup_result_idx, usage, promote)
}
NameLookupResult::NotFound => {
if self.scopes.is_definitely_unreachable() {
return self.insert_binding(key, Binding::Any(AnyStyle::Implicit));
}
if is_special_name(name.id.as_str()) {
self.error(
name.range,
ErrorKind::UnimportedDirective,
format!(
"`{}` must be imported from `typing` for runtime usage",
name
),
);
self.insert_binding(key, Binding::Any(AnyStyle::Error))
} else if self.scopes.in_class_body()
&& let Some(cls) = self.scopes.current_class_key()
{
let suggestion = self.suggest_similar_name(&name.id);
self.insert_binding(
key,
Binding::ClassBodyUnknownName(Box::new(ClassBodyUnknownName {
class_key: cls,
name: name.clone(),
suggestion,
allow_class_body_forward_reference,
})),
)
} else {
// Record a type error and fall back to `Any`. Searching the
// scope for a near-miss is the expensive part of reporting
// this, and it is worth nothing unless the error is kept, so
// it waits until the builder knows that.
self.error_with_detail_from(
name.range,
ErrorKind::UnknownName,
format!("Could not find name `{name}`"),
|| {
self.suggest_similar_name(&name.id)
.map(|suggestion| format!("Did you mean `{suggestion}`?"))
},
);
self.insert_binding(key, Binding::Any(AnyStyle::Error))
}
}
}
}
fn bind_comprehensions(
&mut self,
range: TextRange,
comprehensions: &mut [Comprehension],
usage: &mut Usage,
is_generator: bool,
) {
for (i, comp) in comprehensions.iter_mut().enumerate() {
// Resolve the type of the iteration value *before* binding the target of the iteration.
// This is necessary so that, e.g. `[x for x in x]` correctly uses the outer scope for
// the `in x` lookup.
self.ensure_expr(&mut comp.iter, usage);
if i == 0 {
// Async list/set/dict comprehensions must be inside an async def. Async generator
// expressions are allowed to stand alone because they can have deferred execution.
if comp.is_async && !is_generator && !self.scopes.is_in_async_def() {
self.error(
range,
ErrorKind::InvalidSyntax,
"`async` can only be used inside an async function".to_owned(),
);
}
self.scopes.push(Scope::comprehension(range, is_generator));
}
// Incomplete nested comprehensions can have identical iterators
// for inner and outer loops. It is safe to overwrite it because it literally the same.
let iterable_value_idx = self.insert_binding_overwrite(
Key::Anon(comp.iter.range()),
Binding::IterableValueComprehension(
Box::new(comp.iter.clone()),
IsAsync::new(comp.is_async),
comp.target.range(),
),
);
self.scopes.add_lvalue_to_current_static(&comp.target);
// A comprehension target cannot be annotated, so it is safe to ignore the
// annotation (which is None) and just use a `Forward` here.
self.bind_target_no_expr(&mut comp.target, &|_ann_is_none| {
Binding::Forward(iterable_value_idx)
});
for x in comp.ifs.iter_mut() {
self.ensure_expr(x, &mut Usage::non_pinning_value_from(usage));
let narrow_ops = NarrowOps::from_expr(self, Some(x));
self.bind_narrow_ops(&narrow_ops, NarrowUseLocation::Span(comp.range), usage);
}
}
}
pub fn bind_lambda(&mut self, lambda: &mut ExprLambda, usage: &mut Usage, kind: LambdaKind) {
// Process default values in the enclosing scope before pushing the lambda scope,
// because default values are evaluated at function definition time.
if let Some(parameters) = &mut lambda.parameters {
for x in parameters
.posonlyargs
.iter_mut()
.chain(parameters.args.iter_mut())
.chain(parameters.kwonlyargs.iter_mut())
{
if let Some(default) = x.default.as_deref_mut() {
self.ensure_expr(default, usage);
}
}
}
self.scopes.push(Scope::lambda(
lambda.range,
Identifier::new("<lambda>", lambda.range),
false,
));
if let Some(parameters) = &lambda.parameters {
for x in parameters {
self.bind_lambda_param(x.name(), kind, usage);
}
}
self.ensure_expr(&mut lambda.body, usage);
let (yields_and_returns, _, _, _) = self.scopes.pop_function_scope();
let mut yield_keys = Vec::new();
for (idx, y, is_unreachable) in yields_and_returns.yields {
yield_keys.push(idx);
self.insert_binding_idx(
idx,
if is_unreachable {
BindingYield::Unreachable(y)
} else {
BindingYield::Yield(None, y)
},
);
}
let mut yield_from_keys = Vec::new();
for (idx, y, is_unreachable) in yields_and_returns.yield_froms {
yield_from_keys.push(idx);
self.insert_binding_idx(
idx,
if is_unreachable {
BindingYieldFrom::Unreachable(y)
} else {
// Lambdas cannot be async in Python, so this is always false.
BindingYieldFrom::YieldFrom(None, IsAsync::new(false), y)
},
);
}
if !yield_keys.is_empty() || !yield_from_keys.is_empty() {
self.record_lambda_yield_keys(
lambda.range,
yield_keys.into_boxed_slice(),
yield_from_keys.into_boxed_slice(),
);
}
}
// We want to special-case `self.assertXXX()` methods in unit tests.
// The logic is intentionally syntax-based as we want to avoid checking whether the base type
// is `unittest.TestCase` on every single method invocation.
fn as_assert_in_test(&self, func: &Expr) -> Option<TestAssertion> {
if let Some(class_name) = self.scopes.enclosing_class_name() {
let class_name_str = class_name.as_str();
if !(class_name_str.contains("test") || class_name_str.contains("Test")) {
return None;
}
match func {
Expr::Attribute(ExprAttribute { value, attr, .. })
if let Expr::Name(base_name) = &**value
&& base_name.id.as_str() == "self" =>
{
match attr.id.as_str() {
"assertTrue" => Some(TestAssertion::AssertTrue),
"assertFalse" => Some(TestAssertion::AssertFalse),
"assertIsNone" => Some(TestAssertion::AssertIsNone),
"assertIsNotNone" => Some(TestAssertion::AssertIsNotNone),
"assertIsInstance" => Some(TestAssertion::AssertIsInstance),
"assertNotIsInstance" => Some(TestAssertion::AssertNotIsInstance),
"assertIs" => Some(TestAssertion::AssertIs),
"assertIsNot" => Some(TestAssertion::AssertIsNot),
"assertEqual" => Some(TestAssertion::AssertEqual),
"assertNotEqual" => Some(TestAssertion::AssertNotEqual),
"assertIn" => Some(TestAssertion::AssertIn),
"assertNotIn" => Some(TestAssertion::AssertNotIn),
_ => None,
}
}
_ => None,
}
} else {
None
}
}
/// Synthesize a NamedTuple class from a functional call like `NamedTuple("X", ...)`
/// and insert an anonymous `ClassDef` binding for it. Returns the binding index.
pub fn bind_inline_functional_named_tuple(
&mut self,
call: &mut ExprCall,
kind: SpecialExport,
) -> Option<Idx<Key>> {
let Some(Expr::StringLiteral(name)) = call.arguments.args.first() else {
return None;
};
let class_name = Identifier::new(Name::new(name.value.to_str()), name.range());
let parent = self.scopes.nesting_context();
let (_arg_name, members) = call
.arguments
.args
.split_first_mut()
.expect("caller guarantees at least one arg");
let class_idx = match kind {
SpecialExport::CollectionsNamedTuple => self.synthesize_collections_named_tuple_def(
class_name,
&parent,
&mut call.func,
members,
&mut call.arguments.keywords,
false,
None,
),
SpecialExport::TypingNamedTuple => self.synthesize_typing_named_tuple_def(
class_name,
&parent,
&mut call.func,
members,
false,
None,
),
_ => unreachable!("caller only passes CollectionsNamedTuple or TypingNamedTuple"),
};
Some(self.insert_binding(
Key::Anon(call.range()),
Binding::ClassDef(class_idx, Box::new([])),
))
}
fn record_yield(&mut self, mut x: ExprYield) {
let mut yield_link = self.declare_current_idx(Key::YieldLink(x.range));
let idx = self.idx_for_promise(KeyYield(x.range));
self.ensure_expr_opt(x.value.as_deref_mut(), yield_link.usage());
if let Err(oops_top_level) =
self.scopes
.record_or_reject_yield(idx, x, self.scopes.is_definitely_unreachable())
{
self.insert_binding_idx(idx, BindingYield::Invalid(oops_top_level));
}
self.insert_binding_current(yield_link, Binding::UsageLink(LinkedKey::Yield(idx)));
}
fn record_yield_from(&mut self, mut x: ExprYieldFrom) {
let mut yield_from_link = self.declare_current_idx(Key::YieldLink(x.range));
let idx = self.idx_for_promise(KeyYieldFrom(x.range));
self.ensure_expr(&mut x.value, yield_from_link.usage());
if let Err(oops_top_level) =
self.scopes
.record_or_reject_yield_from(idx, x, self.scopes.is_definitely_unreachable())
{
self.insert_binding_idx(idx, BindingYieldFrom::Invalid(oops_top_level));
}
self.insert_binding_current(
yield_from_link,
Binding::UsageLink(LinkedKey::YieldFrom(idx)),
);
}
/// Execute through the expr, ensuring every name has a binding.
pub fn ensure_expr(&mut self, x: &mut Expr, usage: &mut Usage) {
self.with_semantic_checker(|semantic, context| semantic.visit_expr(x, context));
match x {
Expr::Attribute(attr) => {
self.check_private_attribute_usage(attr);
self.ensure_expr(&mut attr.value, usage);
}
Expr::Subscript(ExprSubscript { value, slice, .. }) => {
// Some subscripts are (or contain) type expressions even when they appear in a
// value context, e.g. `list["A | B"]([x])`. Ensure the slice is bound as a type so
// forward-reference strings are parsed and names inside are bound.
//
// Be careful about attribute access: `dict.__dict__` is an attribute on the class
// `dict` (not a module), and `dict.__dict__["fromkeys"]` is a runtime mappingproxy
// key lookup. Avoid treating those as "type-like subscripts".
let special_export = match &**value {
Expr::Name(_) => self.as_special_export(value),
Expr::Attribute(ExprAttribute { value: base, .. })
if let Expr::Name(base_name) = &**base
&& matches!(
self.scopes.flow_style_for_name(&base_name.id),
Some(FlowStyle::MergeableImport(_) | FlowStyle::ImportAs(_))
) =>
{
self.as_special_export(value)
}
_ => None,
};
if self.is_map_int_tuples_with_provenance(value, special_export) {
self.ensure_expr(&mut *value, usage);
self.bind_map_int_tuples_arguments(
&mut *slice,
None,
false,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
} else if let Some(special_export) = special_export
&& special_export.is_static_type_subscript()
{
self.ensure_expr(&mut *value, usage);
let mut type_usage = Usage::StaticTypeInformation {
is_annotation: false,
};
if special_export == SpecialExport::Annotated
&& let Expr::Tuple(tup) = &mut **slice
&& !tup.is_empty()
{
// Only the first argument to Annotated[...] is a type; the rest are metadata.
self.ensure_type_impl(
&mut tup.elts[0],
None,
false,
false,
&mut type_usage,
false,
);
for elt in tup.elts[1..].iter_mut() {
self.ensure_expr(
elt,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
} else {
self.ensure_type_impl(
&mut *slice,
None,
false,
false,
&mut type_usage,
false,
);
}
} else if self.scopes.has_future_annotations()
&& let Expr::Name(name) = &**value
&& let Some((class_object_idx, FlowStyle::ClassDef { .. })) =
self.scopes.binding_idx_for_name(&name.id)
&& self.class_object_is_generic(class_object_idx)
&& !matches!(
&**slice,
// String-keyed class subscripts, such as Enum member lookup, are runtime
// expressions even when future annotations are active.
Expr::StringLiteral(_)
)
{
self.ensure_expr(&mut *value, usage);
self.ensure_type_impl(
&mut *slice,
None,
false,
false,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
false,
);
} else {
self.ensure_expr(&mut *value, usage);
self.ensure_expr(&mut *slice, usage);
}
}
Expr::If(x) => {
// Ternary operation. We treat it like an if/else statement.
// Process the test before forking so walrus-defined names are
// in the base flow and visible to both branches.
self.ensure_expr(&mut x.test, &mut Usage::non_pinning_value_from(usage));
let static_test = self.sys_info.evaluate_bool_with_sys_info(&x.test);
let narrow_ops = NarrowOps::from_expr(self, Some(&x.test));
self.start_fork_and_branch(x.range);
match static_test {
Some(true) => {
// Skip the `orelse` branch - it typically means a check (e.g. a sys
// version, platform, or TYPE_CHECKING check) where the branch is not
// statically analyzable. However, we still need to check for
// `yield`/`yield from` in the skipped branch, because Python
// determines generator status syntactically at compile time,
// regardless of reachability.
if Ast::expr_contains_yield(&x.orelse) {
self.scopes.mark_has_yield_in_dead_code();
}
self.bind_narrow_ops(
&narrow_ops,
NarrowUseLocation::Span(x.body.range()),
usage,
);
self.ensure_expr(&mut x.body, usage);
self.finish_branch();
}
Some(false) => {
if Ast::expr_contains_yield(&x.body) {
self.scopes.mark_has_yield_in_dead_code();
}
self.abandon_branch();
self.start_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::Span(x.range),
usage,
);
self.ensure_expr(&mut x.orelse, usage);
self.finish_branch();
}
None => {
self.bind_narrow_ops(
&narrow_ops,
NarrowUseLocation::Span(x.body.range()),
usage,
);
self.ensure_expr(&mut x.body, usage);
// Negate the narrow ops for the `orelse`, then merge the Flows.
// TODO(stroxler): We eventually want to drop all narrows but merge values.
self.next_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::Span(x.range),
usage,
);
self.ensure_expr(&mut x.orelse, usage);
self.finish_branch();
}
}
self.finish_exhaustive_fork();
}
Expr::BoolOp(ExprBoolOp {
node_index: _,
range,
op,
values,
}) => {
let mut values = values.iter_mut();
fn get_narrow_ops(myself: &BindingsBuilder, expr: &Expr, op: BoolOp) -> NarrowOps {
let raw_narrow_ops = NarrowOps::from_expr(myself, Some(expr));
match op {
BoolOp::And => {
// Every subsequent value is evaluated only if all previous values were truthy.
raw_narrow_ops
}
BoolOp::Or => {
// Every subsequent value is evaluated only if all previous values were falsy.
raw_narrow_ops.negate()
}
}
}
if let Some(value) = values.next() {
// The first operation runs unconditionally, so any walrus-defined
// names will be added to the base flow.
self.ensure_expr(value, &mut Usage::non_pinning_value_from(usage));
self.start_fork_and_branch(*range);
let mut narrow_ops = get_narrow_ops(self, value, *op);
let short_circuit_trigger: Option<bool> = match op {
BoolOp::And => Some(false),
BoolOp::Or => Some(true),
};
if self.sys_info.evaluate_bool(value) == short_circuit_trigger {
self.scopes.set_definitely_unreachable(true);
}
for value in values {
self.bind_narrow_ops(
&narrow_ops,
NarrowUseLocation::Span(value.range()),
usage,
);
self.ensure_expr(value, &mut Usage::non_pinning_value_from(usage));
let new_narrow_ops = get_narrow_ops(self, value, *op);
narrow_ops.and_all(new_narrow_ops);
if self.sys_info.evaluate_bool(value) == short_circuit_trigger {
self.scopes.set_definitely_unreachable(true);
}
}
// Negate the narrow ops in the base flow and merge.
// TODO(stroxler): We eventually want to drop all narrows but merge values.
// Once we have a way to do that, the negation will be unnecessary.
self.next_branch();
self.bind_narrow_ops(
&narrow_ops.negate(),
NarrowUseLocation::End(*range),
usage,
);
self.finish_branch();
self.finish_bool_op_fork();
}
}
Expr::Call(call) => {
// The `as_special_export` call is load-bearing for
// binding-variant choice — it drives a demand edge to
// `target::Exports`.
let special = self.as_special_export(&call.func);
let call_range = call.range();
match special {
Some(
SpecialExport::CollectionsNamedTuple | SpecialExport::TypingNamedTuple,
) if matches!(call.arguments.args.first(), Some(Expr::StringLiteral(_))) => {
let kind = special.expect("guard already matched");
self.bind_inline_functional_named_tuple(call, kind);
return;
}
Some(SpecialExport::AssertType) if call.arguments.args.len() > 1 => {
// Forward-reference support in the second argument to an `assert_type` call.
self.ensure_expr(&mut call.func, usage);
for (i, arg) in call.arguments.args.iter_mut().enumerate() {
if i == 1 {
self.ensure_type(arg, None);
} else {
self.ensure_expr(arg, usage);
}
}
for kw in call.arguments.keywords.iter_mut() {
self.ensure_expr(&mut kw.value, usage);
}
return;
}
Some(SpecialExport::Cast) if !call.arguments.is_empty() => {
// Forward-reference support in the first argument to a `cast` call.
self.ensure_expr(&mut call.func, usage);
if let Some(arg) = call.arguments.args.first_mut() {
self.ensure_type(arg, None)
}
for arg in call.arguments.args.iter_mut().skip(1) {
self.ensure_expr(arg, usage);
}
for kw in call.arguments.keywords.iter_mut() {
if let Some(id) = &kw.arg
&& id.as_str() == "typ"
{
self.ensure_type(&mut kw.value, None);
} else {
self.ensure_expr(&mut kw.value, usage);
}
}
return;
}
Some(SpecialExport::TypeForm) if !call.arguments.is_empty() => {
// `TypeForm(expr)` — treat the argument as a type expression.
self.ensure_expr(&mut call.func, usage);
if let Some(arg) = call.arguments.args.first_mut() {
self.ensure_type(arg, None)
}
for arg in call.arguments.args.iter_mut().skip(1) {
self.ensure_expr(arg, usage);
}
for kw in call.arguments.keywords.iter_mut() {
self.ensure_expr(&mut kw.value, usage);
}
return;
}
Some(SpecialExport::Super) => {
self.ensure_expr(&mut call.func, usage);
for kw in call.arguments.keywords.iter_mut() {
self.ensure_expr(&mut kw.value, usage);
unexpected_keyword(
&|msg| self.error(call_range, ErrorKind::UnexpectedKeyword, msg),
"super",
kw,
);
}
let nargs = call.arguments.args.len();
let style = if nargs == 0 {
match self.scopes.current_method_and_class() {
Some((method, class_idx)) => {
SuperStyle::ImplicitArgs(class_idx, method)
}
None => {
self.error(
call_range,
ErrorKind::InvalidSuperCall,
"`super` call with no arguments is valid only inside a method"
.to_owned(),
);
SuperStyle::Any
}
}
} else if nargs == 2 {
let mut bind = |expr: &mut Expr| {
self.ensure_expr(expr, usage);
self.insert_binding(
Key::Anon(expr.range()),
Binding::Expr(None, Box::new(expr.clone())),
)
};
let cls_key = bind(&mut call.arguments.args[0]);
let obj_key = bind(&mut call.arguments.args[1]);
SuperStyle::ExplicitArgs(cls_key, obj_key)
} else {
if nargs != 1 {
// Calling super() with one argument is technically legal:
// https://stackoverflow.com/a/30190341.
// This is a very niche use case, and we don't support it aside from not erroring.
self.error(
call_range,
ErrorKind::InvalidSuperCall,
format!("`super` takes at most 2 arguments, got {nargs}"),
);
}
for arg in call.arguments.args.iter_mut() {
self.ensure_expr(arg, usage);
}
SuperStyle::Any
};
self.insert_binding(
Key::SuperInstance(call_range),
Binding::SuperInstance(Box::new((style, call_range))),
);
return;
}
_ => {}
}
// `reveal_type` observes a value without pinning partial types.
// It fires both when imported (`SpecialExport::RevealType`) and when
// used as a bare unimported name, which resolves to `special.is_none()`;
// the latter can't be a `match special` arm, so it's handled here.
let is_unimported_reveal_type = match &*call.func {
Expr::Name(name) if special.is_none() && name.id.as_str() == "reveal_type" => {
self.scopes.binding_idx_for_name(&name.id).is_none()
}
_ => false,
};
if special == Some(SpecialExport::RevealType) || is_unimported_reveal_type {
self.ensure_expr(&mut call.func, usage);
let args = call.arguments.args.split_first_mut();
if let Some((first_arg, remaining_args)) = args {
// `reveal_type` observes its first positional argument.
// Extra arguments are analyzed normally.
if matches!(first_arg, Expr::Name(_)) {
self.ensure_expr(first_arg, &mut Usage::non_pinning_value_from(usage));
} else {
self.ensure_expr(first_arg, usage);
}
for arg in remaining_args {
self.ensure_expr(arg, usage);
}
}
for kw in call.arguments.keywords.iter_mut() {
self.ensure_expr(&mut kw.value, usage);
}
return;
}
// `as_assert_in_test` is *not* a SpecialExport — it is a
// different classification of the callee. Its relative
// order with respect to the Exit/Quit/OsExit branch is
// preserved from the pre-refactor match.
if let Some(test_assert) = self.as_assert_in_test(&call.func)
&& let Some(narrow_op) = test_assert.to_narrow_ops(self, &call.arguments.args)
{
self.ensure_expr(&mut call.func, usage);
for arg in call.arguments.args.iter_mut() {
self.ensure_expr(arg, &mut Usage::non_pinning_value_from(usage));
}
for kw in call.arguments.keywords.iter_mut() {
self.ensure_expr(&mut kw.value, usage);
}
self.bind_narrow_ops(&narrow_op, NarrowUseLocation::Span(call_range), usage);
return;
}
if matches!(
special,
Some(SpecialExport::Exit | SpecialExport::Quit | SpecialExport::OsExit)
) {
x.recurse_mut(&mut |x| self.ensure_expr(x, usage));
// Control flow doesn't proceed after sys.exit(),
// exit(), quit(), or os._exit(). The first three raise `SystemExit`,
// which an enclosing `with` can swallow; `os._exit()` does not.
let kind = if special == Some(SpecialExport::OsExit) {
TerminationKind::Jump
} else {
TerminationKind::Raise
};
self.scopes.mark_flow_termination(kind);
return;
}
// Default: recurse into children as for any other expr.
x.recurse_mut(&mut |x| self.ensure_expr(x, usage));
}
Expr::Named(x) => {
// For scopes defined in terms of Definitions, we should normally already have the name in Static, but
// we still need this for comprehensions, whose scope is defined on-the-fly.
self.scopes.add_lvalue_to_current_static(&x.target);
self.bind_target_with_expr(&mut x.target, &mut x.value, &|expr, ann| {
Binding::Expr(ann, Box::new(expr.clone()))
});
// PEP 572: walrus operators inside comprehensions bind to
// the enclosing (non-comprehension) scope.
if self.scopes.in_comprehension()
&& let Expr::Name(name) = &*x.target
&& let Some(idx) = self.scopes.get_current_flow_idx(&name.id)
{
self.scopes.define_in_enclosing_non_comprehension_scope(
Hashed::new(&name.id),
idx,
FlowStyle::Other,
);
}
}
Expr::Lambda(x) => {
self.bind_lambda(x, usage, LambdaKind::Ordinary);
}
Expr::ListComp(x) => {
self.with_await_context(AwaitContext::General, |this| {
this.bind_comprehensions(x.range, &mut x.generators, usage, false);
this.ensure_expr(&mut x.elt, usage);
this.scopes.pop();
});
}
Expr::SetComp(x) => {
self.with_await_context(AwaitContext::General, |this| {
this.bind_comprehensions(x.range, &mut x.generators, usage, false);
this.ensure_expr(&mut x.elt, usage);
this.scopes.pop();
});
}
Expr::DictComp(x) => {
self.with_await_context(AwaitContext::General, |this| {
this.bind_comprehensions(x.range, &mut x.generators, usage, false);
if let Some(key) = &mut x.key {
this.ensure_expr(key, usage);
}
this.ensure_expr(&mut x.value, usage);
this.scopes.pop();
});
}
Expr::Generator(x) => {
self.with_await_context(AwaitContext::General, |this| {
this.bind_comprehensions(x.range, &mut x.generators, usage, true);
this.with_await_context(AwaitContext::GeneratorElement, |this| {
this.ensure_expr(&mut x.elt, usage);
});
this.scopes.pop();
});
}
Expr::Name(x) => {
let name = Ast::expr_name_identifier(x.clone());
self.ensure_name(&name, usage, None);
}
Expr::Yield(x) => {
self.record_yield(x.clone());
}
Expr::YieldFrom(x) => {
self.record_yield_from(x.clone());
}
Expr::Await(x) => {
self.ensure_expr(&mut x.value, usage);
let in_async_def = self.scopes.is_in_async_def();
let in_generator_element = self.in_generator_await_context();
if !in_async_def
&& !in_generator_element
&& !self.module_info.allows_top_level_await()
{
self.error(
x.range(),
ErrorKind::InvalidSyntax,
"`await` can only be used inside an async function".to_owned(),
);
}
}
_ => {
x.recurse_mut(&mut |x| self.ensure_expr(x, usage));
}
}
}
fn check_private_attribute_usage(&mut self, attr: &ExprAttribute) {
if !Ast::is_mangled_attr(&attr.attr.id) {
return;
}
let expect = PrivateAttributeAccessCheck {
value: (*attr.value).clone(),
attr: attr.attr.clone(),
class_idx: self.scopes.current_method_context(),
};
self.insert_binding(
KeyExpect::PrivateAttributeAccess(attr.attr.range()),
BindingExpect::PrivateAttributeAccess(expect),
);
}
/// Execute through the expr, ensuring every name has a binding.
pub fn ensure_expr_opt(&mut self, x: Option<&mut Expr>, usage: &mut Usage) {
if let Some(x) = x {
self.ensure_expr(x, usage);
}
}
/// Execute through the expr, ensuring every name has a binding.
pub fn ensure_type(
&mut self,
x: &mut Expr,
tparams_builder: Option<&mut LegacyTParamCollector>,
) {
self.ensure_type_with_usage(
x,
tparams_builder,
&mut Usage::StaticTypeInformation {
is_annotation: true,
},
);
}
pub fn ensure_class_member_type(
&mut self,
x: &mut Expr,
tparams_builder: Option<&mut LegacyTParamCollector>,
) {
self.ensure_type_impl(
x,
tparams_builder,
false,
false,
&mut Usage::StaticTypeInformation {
is_annotation: true,
},
true,
);
}
/// Like `ensure_type`, but with a specific usage context. Used by type alias
/// construction sites to pass `Usage::TypeAliasRhs`.
pub fn ensure_type_with_usage(
&mut self,
x: &mut Expr,
tparams_builder: Option<&mut LegacyTParamCollector>,
usage: &mut Usage,
) {
self.ensure_type_impl(x, tparams_builder, false, false, usage, false);
}
pub(super) fn ensure_type_impl(
&mut self,
x: &mut Expr,
mut tparams_builder: Option<&mut LegacyTParamCollector>,
in_string_literal: bool,
check_runtime_name: bool,
usage: &mut Usage,
allow_proxy_method: bool,
) {
fn as_forward_ref<'b>(
literal: &'b ExprStringLiteral,
in_string_literal: bool,
) -> Option<&'b StringLiteral> {
if in_string_literal {
None
} else {
literal.as_single_part_string()
}
}
let expr_range = x.range();
let invalid_proxy_method_use = !allow_proxy_method
&& matches!(usage, Usage::TypeAliasRhs)
&& self.type_expr_is_proxy_method_node(x);
let allow_proxy_method = allow_proxy_method || invalid_proxy_method_use;
if invalid_proxy_method_use {
self.error(
expr_range,
ErrorKind::InvalidAnnotation,
"`ProxyMethod` is only valid as a direct class member annotation".to_owned(),
);
}
match x {
Expr::Name(x) => {
let name = Ast::expr_name_identifier(x.clone());
self.ensure_name_in_type(
&name,
usage,
tparams_builder,
check_runtime_name && !in_string_literal,
in_string_literal
|| self.scopes.has_future_annotations()
|| self.sys_info.version().at_least(3, 14),
);
}
Expr::Subscript(ExprSubscript { value, .. })
if self.as_special_export(value) == Some(SpecialExport::Literal) =>
{
// Don't go inside a literal, since you might find strings which are really strings, not string-types
self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
Expr::Subscript(ExprSubscript { value, slice, .. })
if self.as_special_export(value) == Some(SpecialExport::ProxyMethod) =>
{
self.ensure_type_impl(
&mut *value,
tparams_builder,
in_string_literal,
check_runtime_name,
usage,
true,
);
self.ensure_expr(
&mut *slice,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
Expr::Subscript(ExprSubscript { value, slice, .. })
if self.as_special_export(value) == Some(SpecialExport::Annotated)
&& matches!(&**slice, Expr::Tuple(tup) if !tup.is_empty()) =>
{
// Only go inside the first argument to Annotated, the rest are non-type metadata.
self.ensure_type_impl(
&mut *value,
tparams_builder.as_deref_mut(),
in_string_literal,
check_runtime_name,
usage,
allow_proxy_method,
);
// We can't destructure a mutable Box in the guard, so force unwrapping it here
let tup = slice.as_tuple_expr_mut().unwrap();
self.ensure_type_impl(
&mut tup.elts[0],
tparams_builder,
in_string_literal,
check_runtime_name,
usage,
allow_proxy_method,
);
for e in tup.elts[1..].iter_mut() {
self.ensure_expr(
e,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
}
Expr::Subscript(ExprSubscript { value, slice, .. })
if self.is_map_int_tuples(value) =>
{
self.ensure_type_impl(
&mut *value,
tparams_builder.as_deref_mut(),
in_string_literal,
true,
usage,
allow_proxy_method,
);
self.bind_map_int_tuples_arguments(
&mut *slice,
tparams_builder,
in_string_literal,
usage,
);
}
Expr::Subscript(ExprSubscript { value, slice, .. }) => {
self.ensure_type_impl(
&mut *value,
tparams_builder.as_deref_mut(),
in_string_literal,
true,
usage,
allow_proxy_method,
);
self.ensure_type_impl(
&mut *slice,
tparams_builder,
in_string_literal,
true,
usage,
allow_proxy_method,
);
}
Expr::StringLiteral(expr_literal)
if let Some(literal) = as_forward_ref(expr_literal, in_string_literal) =>
{
if literal.flags.prefix().is_raw() {
self.error(
literal.range(),
ErrorKind::InvalidAnnotation,
"Raw string literals are not allowed in type expressions".to_owned(),
);
}
match Ast::parse_type_literal(expr_literal, self.module_info.contents()) {
Ok(expr) => {
*x = expr;
self.ensure_type_impl(
x,
tparams_builder,
true,
check_runtime_name,
usage,
allow_proxy_method,
);
}
Err(_) => {
// We don't need to emit errors here, because the solving logic expects the expression to resolve to a type, and it will fail.
}
}
}
// Bind the lambda so we don't crash on undefined parameter names.
Expr::Lambda(_) => self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
),
// Bind the call so we generate all expected bindings. See
// test::class_super::test_super_in_base_classes for an example of a SuperInstance
// binding that we crash looking for if we don't do this.
Expr::Call(_) => self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
),
// Bind walrus so we don't crash when looking up the assigned name later.
// Named expressions are not allowed inside type aliases (PEP 695).
Expr::Named(named) => {
if self.scopes.in_type_alias() {
self.error(
named.range,
ErrorKind::InvalidSyntax,
"Named expression cannot be used within a type alias".to_owned(),
);
}
self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
// Bind yield and yield from so we don't crash when checking return type later.
Expr::Yield(_) => {
self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
Expr::YieldFrom(_) => {
self.ensure_expr(
x,
&mut Usage::StaticTypeInformation {
is_annotation: false,
},
);
}
Expr::Attribute(..)
if let Some((base, attrs)) = chase_static_attr_chain(x)
// We assume "args" and "kwargs" are ParamSpec attributes rather than imported TypeVars.
&& attrs.last().id != "args"
&& attrs.last().id != "kwargs" =>
{
// We intercept dotted names (e.g. `mod.T` or `pkg.mod.T`) to check if the
// final attribute is an imported legacy type parameter.
//
// The value part of an attribute access is a module/object reference,
// not a type annotation. For example, in `x: pd.DataFrame`, `pd` is a
// module access — not a type reference — so it should not trigger
// implicit alias validation. We clear `is_annotation` to prevent
// `ImplicitAliasCheck` from being inserted for the value name.
let mut attr_value_usage = match *usage {
Usage::StaticTypeInformation { .. } => Usage::StaticTypeInformation {
is_annotation: false,
},
ref u => u.clone(),
};
self.ensure_simple_attr(
&Ast::expr_name_identifier(base),
attrs,
&mut attr_value_usage,
tparams_builder,
);
}
Expr::Attribute(ExprAttribute { value, attr, .. })
if let Expr::Name(name_expr) = &**value
&& (attr.id == "args" || attr.id == "kwargs") =>
{
// P.args / P.kwargs: resolve P through the legacy tparam collector if P
// is already there (e.g. from `Callable[P, ...]`), but do NOT add P as
// a new legacy type parameter. This prevents P from being incorrectly
// introduced as a tparam when it is only referenced via P.args/P.kwargs
// without being bound by a Callable parameter.
let name = Ast::expr_name_identifier(name_expr.clone());
let id = LegacyTParamId::Name(name.clone());
let resolved = tparams_builder
.as_deref_mut()
.and_then(|tb| self.try_intercept_lookup(tb, &id));
// Same as above: args/kwargs attribute values are not type references.
let mut attr_value_usage = match *usage {
Usage::StaticTypeInformation { .. } => Usage::StaticTypeInformation {
is_annotation: false,
},
ref u => u.clone(),
};
self.ensure_name(&name, &mut attr_value_usage, resolved.and(tparams_builder));
}
Expr::BinOp(ExprBinOp {
left,
op: Operator::BitOr,
right,
range,
..
}) => {
// Check if either side is a string literal BEFORE recursing,
// since ensure_type_impl will parse and replace them.
let left_is_forward_ref = matches!(&**left, Expr::StringLiteral(s) if as_forward_ref(s, in_string_literal).is_some());
let right_is_forward_ref = matches!(&**right, Expr::StringLiteral(s) if as_forward_ref(s, in_string_literal).is_some());
// Recurse into children to handle string literal parsing
self.ensure_type_impl(
left,
tparams_builder.as_deref_mut(),
in_string_literal,
check_runtime_name,
usage,
allow_proxy_method,
);
self.ensure_type_impl(
right,
tparams_builder,
in_string_literal,
check_runtime_name,
usage,
allow_proxy_method,
);
// Only create the check if we're in an executable file, at least one side
// is a forward ref, and we're not in Python 3.14+ or with future annotations
// (which make annotations lazy and avoid the runtime error)
if self.module_info.path().style() == ModuleStyle::Executable
&& (left_is_forward_ref || right_is_forward_ref)
&& !self.sys_info.version().at_least(3, 14)
&& !self.scopes.has_future_annotations()
{
self.insert_binding(
KeyExpect::ForwardRefUnion(*range),
BindingExpect::ForwardRefUnion {
left: Box::new((**left).clone()),
right: Box::new((**right).clone()),
left_is_forward_ref,
right_is_forward_ref,
range: *range,
},
);
}
}
_ => x.recurse_mut(&mut |x| {
self.ensure_type_impl(
x,
tparams_builder.as_deref_mut(),
in_string_literal,
check_runtime_name,
usage,
allow_proxy_method,
)
}),
}
}
fn type_expr_is_proxy_method_node(&self, x: &Expr) -> bool {
match x {
Expr::Name(_) | Expr::Attribute(_) => {
self.as_special_export(x) == Some(SpecialExport::ProxyMethod)
}
Expr::Subscript(subscript) => {
self.as_special_export(&subscript.value) == Some(SpecialExport::ProxyMethod)
}
_ => false,
}
}
/// Execute through the expr, ensuring every name has a binding.
pub fn ensure_type_opt(
&mut self,
x: Option<&mut Expr>,
tparams_builder: Option<&mut LegacyTParamCollector>,
) {
if let Some(x) = x {
self.ensure_type(x, tparams_builder);
}
}
pub fn ensure_and_bind_decorators(
&mut self,
decorators: ThinVec<Decorator>,
usage: &mut Usage,
) -> Vec<Idx<KeyDecorator>> {
let mut decorator_keys = Vec::with_capacity(decorators.len());
for mut x in decorators {
self.ensure_expr(&mut x.expression, usage);
let trailing_name = Ast::decorator_trailing_name(&x.expression).map(Name::new);
let k = self.insert_binding(
KeyDecorator(x.range),
BindingDecorator {
expr: x.expression,
trailing_name,
},
);
decorator_keys.push(k);
}
decorator_keys
}
}