Skip to content

Commit 1a6a8ac

Browse files
committed
[ty] Better invalid-assignment diagnostics
1 parent fb5b8c3 commit 1a6a8ac

10 files changed

+194
-31
lines changed

crates/ty/tests/cli/main.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,24 @@ fn test_quiet_output() -> anyhow::Result<()> {
4141
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
4242

4343
// By default, we emit a diagnostic
44-
assert_cmd_snapshot!(case.command(), @r###"
44+
assert_cmd_snapshot!(case.command(), @r#"
4545
success: false
4646
exit_code: 1
4747
----- stdout -----
4848
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
49-
--> test.py:1:1
49+
--> test.py:1:4
5050
|
5151
1 | x: int = 'foo'
52-
| ^
52+
| --- ^^^^^ Incompatible value of type `Literal["foo"]`
53+
| |
54+
| Declared type
5355
|
5456
info: rule `invalid-assignment` is enabled by default
5557
5658
Found 1 diagnostic
5759
5860
----- stderr -----
59-
"###);
61+
"#);
6062

6163
// With `quiet`, the diagnostic is not displayed, just the summary message
6264
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Invalid assignment diagnostics
2+
3+
<!-- snapshot-diagnostics -->
4+
5+
## Annotated assignment
6+
7+
```py
8+
x: int = "three" # error: [invalid-assignment]
9+
```
10+
11+
## Unannotated assignment
12+
13+
```py
14+
x: int
15+
x = "three" # error: [invalid-assignment]
16+
```
17+
18+
## Named expression
19+
20+
```py
21+
x: int
22+
23+
(x := "three") # error: [invalid-assignment]
24+
```
25+
26+
## Shadowing of classes and functions
27+
28+
See [shadowing.md](./shadowing.md).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Annotated assignment
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | x: int = "three" # error: [invalid-assignment]
16+
```
17+
18+
# Diagnostics
19+
20+
```
21+
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
22+
--> src/mdtest_snippet.py:1:4
23+
|
24+
1 | x: int = "three" # error: [invalid-assignment]
25+
| --- ^^^^^^^ Incompatible value of type `Literal["three"]`
26+
| |
27+
| Declared type
28+
|
29+
info: rule `invalid-assignment` is enabled by default
30+
31+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Named expression
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | x: int
16+
2 |
17+
3 | (x := "three") # error: [invalid-assignment]
18+
```
19+
20+
# Diagnostics
21+
22+
```
23+
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
24+
--> src/mdtest_snippet.py:3:2
25+
|
26+
1 | x: int
27+
2 |
28+
3 | (x := "three") # error: [invalid-assignment]
29+
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
30+
| |
31+
| Declared type `int`
32+
|
33+
info: rule `invalid-assignment` is enabled by default
34+
35+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Unannotated assignment
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | x: int
16+
2 | x = "three" # error: [invalid-assignment]
17+
```
18+
19+
# Diagnostics
20+
21+
```
22+
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
23+
--> src/mdtest_snippet.py:2:1
24+
|
25+
1 | x: int
26+
2 | x = "three" # error: [invalid-assignment]
27+
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
28+
| |
29+
| Declared type `int`
30+
|
31+
info: rule `invalid-assignment` is enabled by default
32+
33+
```

crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado…_(c8ff9e3a079e8bd5).snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ error[invalid-assignment]: Implicit shadowing of class `C`
2626
1 | class C: ...
2727
2 |
2828
3 | C = 1 # error: [invalid-assignment]
29-
| ^
29+
| - ^ Incompatible value of type `Literal[1]`
30+
| |
31+
| Declared type `<class 'C'>`
3032
|
3133
info: Annotate to make it explicit if this is intentional
3234
info: rule `invalid-assignment` is enabled by default

crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh…_(a1515328b775ebc1).snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ error[invalid-assignment]: Implicit shadowing of function `f`
2626
1 | def f(): ...
2727
2 |
2828
3 | f = 1 # error: [invalid-assignment]
29-
| ^
29+
| - ^ Incompatible value of type `Literal[1]`
30+
| |
31+
| Declared type `def f() -> Unknown`
3032
|
3133
info: Annotate to make it explicit if this is intentional
3234
info: rule `invalid-assignment` is enabled by default

crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ In a non-stub file, there's no special treatment of ellipsis literals. An ellips
5959
be assigned if `EllipsisType` is actually assignable to the annotated type.
6060

6161
```py
62-
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
62+
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
6363
def f(x: int = ...) -> None: ...
6464

65-
# error: 1 [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
65+
# error: [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
6666
a: int = ...
6767
b = ...
6868
reveal_type(b) # revealed: EllipsisType
@@ -73,6 +73,6 @@ reveal_type(b) # revealed: EllipsisType
7373
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
7474

7575
```pyi
76-
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
76+
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
7777
def f(x: int = Ellipsis) -> None: ...
7878
```

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,76 +2068,106 @@ pub(crate) fn is_invalid_typed_dict_literal(
20682068
&& matches!(source, AnyNodeRef::ExprDict(_))
20692069
}
20702070

2071-
fn report_invalid_assignment_with_message(
2072-
context: &InferContext,
2071+
fn report_invalid_assignment_with_message<'db, 'ctx: 'db>(
2072+
context: &'ctx InferContext,
20732073
node: AnyNodeRef,
2074-
target_ty: Type,
2074+
target_ty: Type<'db>,
20752075
message: std::fmt::Arguments,
2076-
) {
2077-
let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, node) else {
2078-
return;
2079-
};
2076+
) -> Option<LintDiagnosticGuard<'db, 'ctx>> {
2077+
let builder = context.report_lint(&INVALID_ASSIGNMENT, node)?;
20802078
match target_ty {
20812079
Type::ClassLiteral(class) => {
20822080
let mut diag = builder.into_diagnostic(format_args!(
20832081
"Implicit shadowing of class `{}`",
20842082
class.name(context.db()),
20852083
));
20862084
diag.info("Annotate to make it explicit if this is intentional");
2085+
Some(diag)
20872086
}
20882087
Type::FunctionLiteral(function) => {
20892088
let mut diag = builder.into_diagnostic(format_args!(
20902089
"Implicit shadowing of function `{}`",
20912090
function.name(context.db()),
20922091
));
20932092
diag.info("Annotate to make it explicit if this is intentional");
2093+
Some(diag)
20942094
}
2095+
20952096
_ => {
2096-
builder.into_diagnostic(message);
2097+
let diag = builder.into_diagnostic(message);
2098+
Some(diag)
20972099
}
20982100
}
20992101
}
21002102

21012103
pub(super) fn report_invalid_assignment<'db>(
21022104
context: &InferContext<'db, '_>,
2103-
node: AnyNodeRef,
2105+
target_node: AnyNodeRef,
21042106
definition: Definition<'db>,
21052107
target_ty: Type,
2106-
mut source_ty: Type<'db>,
2108+
mut value_ty: Type<'db>,
21072109
) {
2108-
let value_expr = match definition.kind(context.db()) {
2110+
let definition_kind = definition.kind(context.db());
2111+
let value_node = match definition_kind {
21092112
DefinitionKind::Assignment(def) => Some(def.value(context.module())),
21102113
DefinitionKind::AnnotatedAssignment(def) => def.value(context.module()),
21112114
DefinitionKind::NamedExpression(def) => Some(&*def.node(context.module()).value),
21122115
_ => None,
21132116
};
21142117

2115-
if let Some(value_expr) = value_expr
2116-
&& is_invalid_typed_dict_literal(context.db(), target_ty, value_expr.into())
2118+
if let Some(value_node) = value_node
2119+
&& is_invalid_typed_dict_literal(context.db(), target_ty, value_node.into())
21172120
{
21182121
return;
21192122
}
21202123

21212124
let settings =
2122-
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
2125+
DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, value_ty);
21232126

2124-
if let Some(value_expr) = value_expr {
2127+
if let Some(value_node) = value_node {
21252128
// Re-infer the RHS of the annotated assignment, ignoring the type context for more precise
21262129
// error messages.
2127-
source_ty =
2128-
infer_isolated_expression(context.db(), definition.scope(context.db()), value_expr);
2130+
value_ty =
2131+
infer_isolated_expression(context.db(), definition.scope(context.db()), value_node);
21292132
}
21302133

2131-
report_invalid_assignment_with_message(
2134+
let Some(mut diag) = report_invalid_assignment_with_message(
21322135
context,
2133-
node,
2136+
value_node.map(AnyNodeRef::from).unwrap_or(target_node),
21342137
target_ty,
21352138
format_args!(
21362139
"Object of type `{}` is not assignable to `{}`",
2137-
source_ty.display_with(context.db(), settings.clone()),
2140+
value_ty.display_with(context.db(), settings.clone()),
21382141
target_ty.display_with(context.db(), settings)
21392142
),
2140-
);
2143+
) else {
2144+
return;
2145+
};
2146+
2147+
if value_node.is_some() {
2148+
match definition_kind {
2149+
DefinitionKind::AnnotatedAssignment(assignment) => {
2150+
// For annotated assignments, just point to the annotation in the source code.
2151+
diag.annotate(
2152+
context
2153+
.secondary(assignment.annotation(context.module()))
2154+
.message("Declared type"),
2155+
);
2156+
}
2157+
_ => {
2158+
// Otherwise, annotate the target with its declared type.
2159+
diag.annotate(context.secondary(target_node).message(format_args!(
2160+
"Declared type `{}`",
2161+
target_ty.display(context.db()),
2162+
)));
2163+
}
2164+
}
2165+
2166+
diag.set_primary_message(format_args!(
2167+
"Incompatible value of type `{}`",
2168+
value_ty.display(context.db()),
2169+
));
2170+
}
21412171
}
21422172

21432173
pub(super) fn report_invalid_attribute_assignment(

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7744,7 +7744,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
77447744

77457745
self.infer_expression(target, TypeContext::default());
77467746

7747-
self.add_binding(named.into(), definition, |builder, tcx| {
7747+
self.add_binding(named.target.as_ref().into(), definition, |builder, tcx| {
77487748
builder.infer_expression(value, tcx)
77497749
})
77507750
}

0 commit comments

Comments
 (0)