Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
# error: [unsupported-operator]
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
p: int | f"foo",
# error: [invalid-type-form] "Slices are not allowed in type expressions"
# error: [invalid-type-form] "Invalid subscript"
q: [1, 2, 3][1:2],
):
Expand Down Expand Up @@ -159,6 +158,37 @@ def invalid_binary_operators(
reveal_type(l) # revealed: Unknown
```

## Error recovery upon encountering invalid AST nodes

Upon encountering an invalid-in-type-expression AST node, we try to avoid cascading diagnostics. For
example, in this snippet, we only report the the outer list literal is invalid, and ignore the fact
that there is also an invalid list literal inside the outer list literal node:

```py
# error: [invalid-type-form]
x: [[int]]
```

However, runtime errors inside invalid AST nodes are still reported -- these errors are more serious
than just "typing spec pedantry":

```py
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
# error: [unresolved-reference] "Name `foo` used when not defined"
x: [[foo]]
```

But we avoid false-positive diagnostics regarding unresolved references inside string annotations if
we detect that the string annotation is an invalid type form. These diagnostics would just add
noise, since stringized annotations are never executed at runtime. The following snippet causes us
to emit `invalid-type-form`, but we ignore that `foo` is an "unresolved reference" inside the string
annotation:

```py
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
x: "[[foo]]"
```

## Multiple starred expressions in a `tuple` specialization

<!-- snapshot-diagnostics -->
Expand Down Expand Up @@ -246,7 +276,6 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
l: "(yield 1)", # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
m: "1 < 2", # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
n: "bar()", # error: [invalid-type-form] "Function calls are not allowed in type expressions"
# error: [invalid-type-form] "Slices are not allowed in type expressions"
# error: [invalid-type-form] "Invalid subscript"
o: "[1, 2, 3][1:2]",
):
Expand Down Expand Up @@ -282,7 +311,7 @@ def _(
d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions"
e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions"
f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions"
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?"
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
g: [int, str],
# error: [invalid-type-form] "Tuple literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?"
h: (int, str),
Expand All @@ -303,7 +332,6 @@ class name_0[name_2: [int]]:
pass

# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Dict literals are not allowed in type expressions"
class name_4[name_1: [{}]]:
pass
```
Expand Down Expand Up @@ -340,16 +368,15 @@ from PIL import Image
def g(x: Image): ... # error: [invalid-type-form]
```

### List-literal used when you meant to use a list or tuple
### List-literal used when you meant to use a list

```py
def _(
x: [int], # error: [invalid-type-form]
) -> [int]: # error: [invalid-type-form]
return x
```

```py
# No special hints for these: it's unclear what the user meant:
def _(
x: [int, str], # error: [invalid-type-form]
) -> [int, str]: # error: [invalid-type-form]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ from other import Literal
#
# ?
#
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Invalid subscript of object of type `_SpecialForm` in type expression"
a1: Literal[26]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,8 @@ o: "1 < 2"
# error: [invalid-type-form]
p: "call()"
# error: [invalid-type-form] "List literals are not allowed"
# error: [invalid-type-form] "Int literals are not allowed"
# error: [invalid-type-form] "Int literals are not allowed"
r: "[1, 2]"
# error: [invalid-type-form] "Tuple literals are not allowed"
# error: [invalid-type-form] "Int literals are not allowed"
# error: [invalid-type-form] "Int literals are not allowed"
s: "(1, 2)"
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ from typing_extensions import Self, TypeAlias, TypeVar
T = TypeVar("T")

# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
# error: [unbound-type-variable]
X: TypeAlias[T] = int

class Foo[T]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,15 @@ def _(doubly_specialized: DoublySpecialized):
# error: [not-subscriptable] "Cannot subscript non-generic type `<class 'list[int]'>`"
List = list[int][int]

def _(doubly_specialized: List):
# TODO: one error would be enough here
#
# error: [not-subscriptable] "Cannot subscript non-generic type `<class 'list[int]'>`"
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
WorseList = list[int][0]

def _(doubly_specialized: List, doubly_specialized_2: WorseList):
reveal_type(doubly_specialized) # revealed: Unknown
reveal_type(doubly_specialized_2) # revealed: Unknown

Tuple = tuple[int, str]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ a = 123
def f(_) -> TypeGuard[int, str]: ...

# error: [invalid-type-form] "Special form `typing.TypeIs` expected exactly one type parameter"
# error: [invalid-type-form] "Variable of type `Literal[123]` is not allowed in a type expression"
def g(_) -> TypeIs[a, str]: ...

reveal_type(f(0)) # revealed: Unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ expression: snapshot
---

---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - List-literal used when you meant to use a list or tuple
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - List-literal used when you meant to use a list
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---

Expand All @@ -13,14 +13,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
## mdtest_snippet.py

```
1 | def _(
2 | x: [int], # error: [invalid-type-form]
3 | ) -> [int]: # error: [invalid-type-form]
4 | return x
5 | def _(
6 | x: [int, str], # error: [invalid-type-form]
7 | ) -> [int, str]: # error: [invalid-type-form]
8 | return x
1 | def _(
2 | x: [int], # error: [invalid-type-form]
3 | ) -> [int]: # error: [invalid-type-form]
4 | return x
5 |
6 | # No special hints for these: it's unclear what the user meant:
7 | def _(
8 | x: [int, str], # error: [invalid-type-form]
9 | ) -> [int, str]: # error: [invalid-type-form]
10 | return x
```

# Diagnostics
Expand Down Expand Up @@ -50,7 +52,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a typ
3 | ) -> [int]: # error: [invalid-type-form]
| ^^^^^ Did you mean `list[int]`?
4 | return x
5 | def _(
|
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
Expand All @@ -60,15 +61,15 @@ info: rule `invalid-type-form` is enabled by default

```
error[invalid-type-form]: List literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:6:8
|
4 | return x
5 | def _(
6 | x: [int, str], # error: [invalid-type-form]
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
7 | ) -> [int, str]: # error: [invalid-type-form]
8 | return x
|
--> src/mdtest_snippet.py:8:8
|
6 | # No special hints for these: it's unclear what the user meant:
7 | def _(
8 | x: [int, str], # error: [invalid-type-form]
| ^^^^^^^^^^
9 | ) -> [int, str]: # error: [invalid-type-form]
10 | return x
|
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
info: rule `invalid-type-form` is enabled by default
Expand All @@ -77,14 +78,14 @@ info: rule `invalid-type-form` is enabled by default

```
error[invalid-type-form]: List literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:7:6
|
5 | def _(
6 | x: [int, str], # error: [invalid-type-form]
7 | ) -> [int, str]: # error: [invalid-type-form]
| ^^^^^^^^^^ Did you mean `tuple[int, str]`?
8 | return x
|
--> src/mdtest_snippet.py:9:6
|
7 | def _(
8 | x: [int, str], # error: [invalid-type-form]
9 | ) -> [int, str]: # error: [invalid-type-form]
| ^^^^^^^^^^
10 | return x
|
info: See the following page for a reference on valid type expressions:
info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
info: rule `invalid-type-form` is enabled by default
Expand Down
66 changes: 66 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,72 @@ impl<'db> Type<'db> {
}
}

/// Return `true` if `self` is a type that is suitable for displaying
/// in a "Did you mean...?" hint message in diagnostics
fn is_hintable(&self, db: &'db dyn Db) -> bool {
match self {
Type::NominalInstance(_)
| Type::NewTypeInstance(_)
| Type::LiteralValue(_)
| Type::TypeAlias(_) => true,

Type::Intersection(_)
| Type::Divergent(_)
| Type::SpecialForm(_)
| Type::BoundSuper(_)
| Type::BoundMethod(_)
| Type::KnownBoundMethod(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(_)
| Type::TypeGuard(_)
| Type::PropertyInstance(_)
| Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::WrapperDescriptor(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::ClassLiteral(_)
| Type::GenericAlias(_)
| Type::KnownInstance(_) => false,

// `Never` is spellable and could result from an explicit type annotation,
// but also could just be the result of us inferring an unreachable region.
// Best to avoid showing it in hints.
Type::Never => false,

// All `Callable` types are spellable in some way,
// but they're generally not spellable with the syntax we use by default
// in our type display
Type::Callable(_) => false,

Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Class(_) => true,
SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic).is_hintable(db),
SubclassOfInner::TypeVar(tvar) => Type::TypeVar(tvar).is_hintable(db),
},

Type::TypeVar(tvar) => tvar.typevar(db).definition(db).is_some(),

Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_hintable(db)),

Type::TypedDict(td) => td.defining_class().is_some(),

Type::ProtocolInstance(ProtocolInstanceType { inner, .. }) => !inner.is_synthesized(),

Type::Dynamic(dynamic) => match dynamic {
DynamicType::Any => true,
DynamicType::Unknown
| DynamicType::UnknownGeneric(_)
| DynamicType::UnspecializedTypeVar
| DynamicType::TodoUnpack
| DynamicType::TodoTypeVarTuple
| DynamicType::Todo(_)
| DynamicType::TodoStarredExpression => false,
},
}
}

/// If the type is a union (or a type alias that resolves to a union), filters union elements
/// based on the provided predicate.
///
Expand Down
8 changes: 6 additions & 2 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.context.in_stub()
}

fn in_string_annotation(&self) -> bool {
self.deferred_state.in_string_annotation()
}

/// Returns `true` if `expr` is a call to a known diagnostic function
/// (e.g., `reveal_type` or `assert_type`) whose return value should not
/// trigger the `unused-awaitable` lint.
Expand Down Expand Up @@ -7866,7 +7870,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
place_from_bindings(db, use_def.reachable_bindings(place_id)).place
} else {
assert!(
self.deferred_state.in_string_annotation(),
self.in_string_annotation(),
"Expected the place table to create a place for every valid PlaceExpr node"
);
Place::Undefined
Expand Down Expand Up @@ -9330,7 +9334,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
///
/// The inference results can be merged into the current inference region using
/// [`TypeInferenceBuilder::extend`].
fn speculate(&mut self) -> Self {
fn speculate(&self) -> Self {
let Self {
region,
index,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
{
builder.into_diagnostic("Type expressions cannot use bytes literal");
}
if !self.in_string_annotation() {
self.infer_bytes_literal_expression(bytes);
}
TypeAndQualifiers::declared(Type::unknown())
}

ast::Expr::FString(fstring) => {
if let Some(builder) = self.context.report_lint(&FSTRING_TYPE_ANNOTATION, fstring) {
builder.into_diagnostic("Type expressions cannot use f-strings");
}
self.infer_fstring_expression(fstring);
if !self.in_string_annotation() {
self.infer_fstring_expression(fstring);
}
TypeAndQualifiers::declared(Type::unknown())
}

Expand Down
Loading
Loading