[ty] Fallback to Unknown if no type is stored for an expression (#19517)

## Summary

See discussion at
https://github.com/astral-sh/ruff/pull/19478/files#r2223870292

Fixes https://github.com/astral-sh/ty/issues/865

## Test Plan

Added one mdtest for invalid Callable annotation; removed `pull-types:
skip` from that test file.

Co-authored-by: lipefree <willy.ngo.2000@gmail.com>
This commit is contained in:
Carl Meyer 2025-07-24 19:05:32 -07:00 committed by GitHub
parent c8c80e054e
commit ae9d450b5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 35 deletions

View file

@ -0,0 +1,5 @@
from typing import Callable
def a(x: Callable[.., None]): ...
def b(x: Callable[..

View file

@ -58,8 +58,6 @@ def _(c: Callable[[int, 42, str, False], None]):
### Missing return type ### Missing return type
<!-- pull-types:skip -->
Using a parameter list: Using a parameter list:
```py ```py
@ -90,6 +88,21 @@ def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable`
reveal_type(c) # revealed: (...) -> Unknown reveal_type(c) # revealed: (...) -> Unknown
``` ```
### Invalid parameters and return type
```py
from typing import Callable
# fmt: off
def _(c: Callable[
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
{1, 2}, 2 # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
]
):
reveal_type(c) # revealed: (...) -> Unknown
```
### More than two arguments ### More than two arguments
We can't reliably infer the callable type if there are more then 2 arguments because we don't know We can't reliably infer the callable type if there are more then 2 arguments because we don't know

View file

@ -452,14 +452,9 @@ impl<'db> ScopeInference<'db> {
self.extra.as_deref().map(|extra| &extra.diagnostics) self.extra.as_deref().map(|extra| &extra.diagnostics)
} }
#[track_caller]
pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> { pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> {
self.try_expression_type(expression).expect( self.try_expression_type(expression)
"Failed to retrieve the inferred type for an `ast::Expr` node \ .unwrap_or_else(Type::unknown)
passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` \
should infer and store types for all `ast::Expr` nodes in any `TypeInference` \
region it analyzes.",
)
} }
pub(crate) fn try_expression_type( pub(crate) fn try_expression_type(
@ -541,14 +536,9 @@ impl<'db> DefinitionInference<'db> {
} }
} }
#[track_caller]
pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> { pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> {
self.try_expression_type(expression).expect( self.try_expression_type(expression)
"Failed to retrieve the inferred type for an `ast::Expr` node \ .unwrap_or_else(Type::unknown)
passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` \
should infer and store types for all `ast::Expr` nodes in any `TypeInference` \
region it analyzes.",
)
} }
pub(crate) fn try_expression_type( pub(crate) fn try_expression_type(
@ -675,14 +665,9 @@ impl<'db> ExpressionInference<'db> {
.or_else(|| self.fallback_type()) .or_else(|| self.fallback_type())
} }
#[track_caller]
pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> { pub(crate) fn expression_type(&self, expression: impl Into<ExpressionNodeKey>) -> Type<'db> {
self.try_expression_type(expression).expect( self.try_expression_type(expression)
"Failed to retrieve the inferred type for an `ast::Expr` node \ .unwrap_or_else(Type::unknown)
passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` \
should infer and store types for all `ast::Expr` nodes in any `TypeInference` \
region it analyzes.",
)
} }
fn is_cycle_callback(&self) -> bool { fn is_cycle_callback(&self) -> bool {
@ -959,19 +944,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.in_stub() self.context.in_stub()
} }
/// Get the already-inferred type of an expression node. /// Get the already-inferred type of an expression node, or Unknown.
///
/// ## Panics
/// If the expression is not within this region, or if no type has yet been inferred for
/// this node.
#[track_caller]
fn expression_type(&self, expr: &ast::Expr) -> Type<'db> { fn expression_type(&self, expr: &ast::Expr) -> Type<'db> {
self.try_expression_type(expr).expect( self.try_expression_type(expr).unwrap_or_else(Type::unknown)
"Failed to retrieve the inferred type for an `ast::Expr` node \
passed to `TypeInference::expression_type()`. The `TypeInferenceBuilder` \
should infer and store types for all `ast::Expr` nodes in any `TypeInference` \
region it analyzes.",
)
} }
fn try_expression_type(&self, expr: &ast::Expr) -> Option<Type<'db>> { fn try_expression_type(&self, expr: &ast::Expr) -> Option<Type<'db>> {