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
280 changes: 109 additions & 171 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,15 @@ def _(
return x
```

### Dict-literal or set-literal when you meant to use `dict[]`/`set[]`

```py
def _(
x: {int: str}, # error: [invalid-type-form]
y: {str}, # error: [invalid-type-form]
): ...
```

### Special-cased diagnostic for `callable` used in a type expression

```py
Expand All @@ -428,3 +437,18 @@ def _(
def decorator(fn: callable) -> callable:
return fn
```

### AST nodes that are only valid inside `Literal`

```py
def bad(
# error: [invalid-type-form]
a: 42,
# error: [invalid-type-form]
b: b"42",
# error: [invalid-type-form]
c: True,
# error: [invalid-syntax-in-forward-annotation]
d: "invalid syntax",
): ...
```
60 changes: 36 additions & 24 deletions crates/ty_python_semantic/resources/mdtest/annotations/string.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,32 +217,44 @@ class Foo: ...

```py
def f1(
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
# error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions"
a: r"int",
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
b: f"int",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
c: b"int",
d: "int",
# error: [raw-string-type-annotation] "Raw string literals are not allowed in type expressions"
b: list[r"int"],
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
c: f"int",
# error: [invalid-type-form] "F-strings are not allowed in type expressions"
d: list[f"int"],
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the message differ here because bytes literals are sometimes allowed in type expressions (e.g., within Literal)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, exactly. It's not true to say they're always invalid in type expressions -- they're just only valid in certain specific contexts

e: b"int",
f: "int",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
e: "in" "t",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
f: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
g: "\x69nt",
h: """int""",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
i: "b'int'",
g: "in" "t",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
h: list["in" "t"],
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions"
i: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Escape characters are not allowed in type expressions"
j: "\x69nt",
k: """int""",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
l: "b'int'",
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
m: list[b"int"],
): # fmt:skip
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(b) # revealed: list[Unknown]
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: int
reveal_type(d) # revealed: list[Unknown]
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
reveal_type(f) # revealed: int
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: int
reveal_type(h) # revealed: list[Unknown]
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: int
reveal_type(l) # revealed: Unknown
reveal_type(m) # revealed: list[Unknown]
```

## Various string kinds in `typing.Literal`
Expand Down Expand Up @@ -305,17 +317,17 @@ shouldn't panic.

```py
# Regression test for https://github.com/astral-sh/ty/issues/1865
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
# error: [fstring-type-annotation]
# error: [invalid-type-form]
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"

# error: [invalid-type-form]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - AST nodes that are only valid inside `Literal`
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---

# Python source files

## mdtest_snippet.py

```
1 | def bad(
2 | # error: [invalid-type-form]
3 | a: 42,
4 | # error: [invalid-type-form]
5 | b: b"42",
6 | # error: [invalid-type-form]
7 | c: True,
8 | # error: [invalid-syntax-in-forward-annotation]
9 | d: "invalid syntax",
10 | ): ...
```

# Diagnostics

```
error[invalid-type-form]: Int literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:3:8
|
1 | def bad(
2 | # error: [invalid-type-form]
3 | a: 42,
| ^^ Did you mean `typing.Literal[42]`?
4 | # error: [invalid-type-form]
5 | b: b"42",
|
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

```

```
error[invalid-type-form]: Bytes literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:5:8
|
3 | a: 42,
4 | # error: [invalid-type-form]
5 | b: b"42",
| ^^^^^ Did you mean `typing.Literal[b"42"]`?
6 | # error: [invalid-type-form]
7 | c: True,
|
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

```

```
error[invalid-type-form]: Boolean literals are not allowed in this context in a type expression
--> src/mdtest_snippet.py:7:8
|
5 | b: b"42",
6 | # error: [invalid-type-form]
7 | c: True,
| ^^^^ Did you mean `typing.Literal[True]`?
8 | # error: [invalid-syntax-in-forward-annotation]
9 | d: "invalid syntax",
|
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

```

```
error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation: Unexpected token at the end of an expression
--> src/mdtest_snippet.py:9:8
|
7 | c: True,
8 | # error: [invalid-syntax-in-forward-annotation]
9 | d: "invalid syntax",
| ^^^^^^^^^^^^^^^^ Did you mean `typing.Literal["invalid syntax"]`?
10 | ): ...
|
info: rule `invalid-syntax-in-forward-annotation` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: invalid.md - Tests for invalid types in type expressions - Diagnostics for common errors - Dict-literal or set-literal when you meant to use `dict[]`/`set[]`
mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/invalid.md
---

# Python source files

## mdtest_snippet.py

```
1 | def _(
2 | x: {int: str}, # error: [invalid-type-form]
3 | y: {str}, # error: [invalid-type-form]
4 | ): ...
```

# Diagnostics

```
error[invalid-type-form]: Dict literals are not allowed in type expressions
--> src/mdtest_snippet.py:2:8
|
1 | def _(
2 | x: {int: str}, # error: [invalid-type-form]
| ^^^^^^^^^^ Did you mean `dict[int, str]`?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rad

3 | y: {str}, # error: [invalid-type-form]
4 | ): ...
|
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

```

```
error[invalid-type-form]: Set literals are not allowed in type expressions
--> src/mdtest_snippet.py:3:8
|
1 | def _(
2 | x: {int: str}, # error: [invalid-type-form]
3 | y: {str}, # error: [invalid-type-form]
| ^^^^^ Did you mean `set[str]`?
4 | ): ...
|
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

```
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
86 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
87 | # error: [invalid-type-form]
88 | Bad10 = TypedDict("Bad10", {name: 42})
| ^^
| ^^ Did you mean `typing.Literal[42]`?
89 |
90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
|
Expand Down Expand Up @@ -563,7 +563,7 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type
90 | # error: [invalid-argument-type] "Expected a string-literal key in the `fields` dict of `TypedDict()`"
91 | # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
92 | class Bad11(TypedDict("Bad11", {name: 42})): ...
| ^^
| ^^ Did you mean `typing.Literal[42]`?
93 |
94 | # error: [invalid-argument-type] "Invalid argument to parameter `typename` of `TypedDict()`: Expected `str`, found `Literal[123]`"
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-z

```py
# fmt: off
def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # ty: ignore[fstring-type-annotation, byte-string-type-annotation]
def test(a: f"f-string type annotation", b: unresolved_ref): ... # ty: ignore[invalid-type-form, unresolved-reference]
```

## Can't suppress syntax errors
Expand Down
7 changes: 2 additions & 5 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ use crate::types::infer::UnsupportedComparisonError;
use crate::types::overrides::MethodKind;
use crate::types::protocol_class::ProtocolMember;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::tuple::TupleSpec;
use crate::types::typed_dict::TypedDictSchema;
Expand Down Expand Up @@ -160,9 +159,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_LEGACY_POSITIONAL_PARAMETER);

// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION);
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION);
registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION);
registry.register_lint(&RAW_STRING_TYPE_ANNOTATION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ use crate::types::diagnostic::{INVALID_TYPE_FORM, REDUNDANT_FINAL_CLASSVAR};
use crate::types::infer::builder::InferenceFlags;
use crate::types::infer::builder::subscript::AnnotatedExprContext;
use crate::types::infer::nearest_enclosing_class;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
};
use crate::types::string_annotation::parse_string_annotation;
use crate::types::{
SpecialFormType, Type, TypeAndQualifiers, TypeContext, TypeQualifier, TypeQualifiers, todo_type,
};
Expand Down Expand Up @@ -161,34 +159,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
// String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),

// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => TypeAndQualifiers::declared(
self.infer_starred_expression(starred, TypeContext::default()),
),

ast::Expr::BytesLiteral(bytes) => {
if let Some(builder) = self
.context
.report_lint(&BYTE_STRING_TYPE_ANNOTATION, bytes)
{
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");
}
if !self.in_string_annotation() {
self.infer_fstring_expression(fstring);
}
TypeAndQualifiers::declared(Type::unknown())
}

ast::Expr::Attribute(attribute) => {
if !is_dotted_name(annotation) {
return TypeAndQualifiers::declared(self.infer_type_expression(annotation));
Expand Down Expand Up @@ -357,8 +327,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
}

// All other annotation expressions are (possibly) valid type expressions, so handle
// them there instead.
// Fallback to `infer_type_expression_no_store` for everything else
type_expr => {
TypeAndQualifiers::declared(self.infer_type_expression_no_store(type_expr))
}
Expand Down
Loading
Loading