From 96dbe609eb7f71de355873f0df688c756b1ab2ce Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 17 Dec 2025 11:29:55 -0500 Subject: [PATCH] Respect deferred values in keyword arguments et al for .pyi files --- .../resources/mdtest/annotations/deferred.md | 90 +++++++++++++++++++ .../src/types/infer/builder.rs | 15 ++++ 2 files changed, 105 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md index 89b9324ea4..0a1f8ea912 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/deferred.md @@ -205,3 +205,93 @@ class B: class A(B): ... class B: ... ``` + +## Default argument values + +### Not deferred in regular files + +```py +# error: [unresolved-reference] +def f(mode: int = ParseMode.test): + pass + +class ParseMode: + test = 1 +``` + +### Deferred in stub files + +Forward references in default argument values are allowed in stub files. + +```pyi +def f(mode: int = ParseMode.test): ... + +class ParseMode: + test: int +``` + +### Undefined names are still errors in stub files + +```pyi +# error: [unresolved-reference] +def f(mode: int = NeverDefined.test): ... +``` + +## Class keyword arguments + +### Not deferred in regular files + +```py +# error: [unresolved-reference] +class Foo(metaclass=SomeMeta): + pass + +class SomeMeta(type): + pass +``` + +### Deferred in stub files + +Forward references in class keyword arguments are allowed in stub files. + +```pyi +class Foo(metaclass=SomeMeta): ... + +class SomeMeta(type): ... +``` + +### Undefined names are still errors in stub files + +```pyi +# error: [unresolved-reference] +class Foo(metaclass=NeverDefined): ... +``` + +## Lambda default argument values + +### Not deferred in regular files + +```py +# error: [unresolved-reference] +f = lambda x=Foo(): x + +class Foo: + pass +``` + +### Deferred in stub files + +Forward references in lambda default argument values are allowed in stub files. + +```pyi +f = lambda x=Foo(): x + +class Foo: ... +``` + +### Undefined names are still errors in stub files + +```pyi +# error: [unresolved-reference] +f = lambda x=NeverDefined(): x +``` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c93269ade2..ac6d092c08 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2358,12 +2358,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { decorator_types_and_nodes.push((decorator_type, decorator)); } + // In stub files, default values may reference names that are defined later in the file. + let in_stub = self.in_stub(); + let previous_deferred_state = std::mem::replace(&mut self.deferred_state, in_stub.into()); for default in parameters .iter_non_variadic_params() .filter_map(|param| param.default.as_deref()) { self.infer_expression(default, TypeContext::default()); } + self.deferred_state = previous_deferred_state; // If there are type params, parameters and returns are evaluated in that scope. Otherwise, // we always defer the inference of the parameters and returns. That ensures that we do not @@ -2998,9 +3002,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // if there are type parameters, then the keywords and bases are within that scope // and we don't need to run inference here if type_params.is_none() { + // In stub files, keyword values may reference names that are defined later in the file. + let in_stub = self.in_stub(); + let previous_deferred_state = + std::mem::replace(&mut self.deferred_state, in_stub.into()); for keyword in class_node.keywords() { self.infer_expression(&keyword.value, TypeContext::default()); } + self.deferred_state = previous_deferred_state; // Inference of bases deferred in stubs, or if any are string literals. if self.in_stub() || class_node.bases().iter().any(contains_string_literal) { @@ -8322,6 +8331,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { body: _, } = lambda_expression; + // In stub files, default values may reference names that are defined later in the file. + let in_stub = self.in_stub(); + let previous_deferred_state = std::mem::replace(&mut self.deferred_state, in_stub.into()); + let parameters = if let Some(parameters) = parameters { let positional_only = parameters .posonlyargs @@ -8387,6 +8400,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Parameters::empty() }; + self.deferred_state = previous_deferred_state; + // TODO: Useful inference of a lambda's return type will require a different approach, // which does the inference of the body expression based on arguments at each call site, // rather than eagerly computing a return type without knowing the argument types.