Skip to content

Commit 8e78adc

Browse files
committed
Detect inferred args that don't satisfy bound/constraints
1 parent 837489d commit 8e78adc

File tree

5 files changed

+343
-20
lines changed

5 files changed

+343
-20
lines changed

crates/red_knot_python_semantic/resources/mdtest/generics/functions.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,39 @@ def f[T](x: list[T]) -> T:
7171
reveal_type(f([1.0, 2.0])) # revealed: Unknown
7272
```
7373

74+
## Inferring a bound typevar
75+
76+
<!-- snapshot-diagnostics -->
77+
78+
```py
79+
from typing_extensions import reveal_type
80+
81+
def f[T: int](x: T) -> T:
82+
return x
83+
84+
reveal_type(f(1)) # revealed: Literal[1]
85+
reveal_type(f(True)) # revealed: Literal[True]
86+
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
87+
reveal_type(f("string")) # revealed: Unknown
88+
```
89+
90+
## Inferring a constrained typevar
91+
92+
<!-- snapshot-diagnostics -->
93+
94+
```py
95+
from typing_extensions import reveal_type
96+
97+
def f[T: (int, None)](x: T) -> T:
98+
return x
99+
100+
reveal_type(f(1)) # revealed: int
101+
reveal_type(f(True)) # revealed: int
102+
reveal_type(f(None)) # revealed: None
103+
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
104+
reveal_type(f("string")) # revealed: Unknown
105+
```
106+
74107
## Typevar constraints
75108

76109
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: functions.md - Generic functions - Inferring a bound typevar
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import reveal_type
16+
2 |
17+
3 | def f[T: int](x: T) -> T:
18+
4 | return x
19+
5 |
20+
6 | reveal_type(f(1)) # revealed: Literal[1]
21+
7 | reveal_type(f(True)) # revealed: Literal[True]
22+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
23+
9 | reveal_type(f("string")) # revealed: Unknown
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
info: revealed-type: Revealed type
30+
--> src/mdtest_snippet.py:6:1
31+
|
32+
4 | return x
33+
5 |
34+
6 | reveal_type(f(1)) # revealed: Literal[1]
35+
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
36+
7 | reveal_type(f(True)) # revealed: Literal[True]
37+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bo...
38+
|
39+
40+
```
41+
42+
```
43+
info: revealed-type: Revealed type
44+
--> src/mdtest_snippet.py:7:1
45+
|
46+
6 | reveal_type(f(1)) # revealed: Literal[1]
47+
7 | reveal_type(f(True)) # revealed: Literal[True]
48+
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
49+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
50+
9 | reveal_type(f("string")) # revealed: Unknown
51+
|
52+
53+
```
54+
55+
```
56+
error: lint:invalid-argument-type: Argument to this function is incorrect
57+
--> src/mdtest_snippet.py:9:15
58+
|
59+
7 | reveal_type(f(True)) # revealed: Literal[True]
60+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
61+
9 | reveal_type(f("string")) # revealed: Unknown
62+
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`
63+
|
64+
info: Type variable defined here
65+
--> src/mdtest_snippet.py:3:7
66+
|
67+
1 | from typing_extensions import reveal_type
68+
2 |
69+
3 | def f[T: int](x: T) -> T:
70+
| ^^^^^^
71+
4 | return x
72+
|
73+
74+
```
75+
76+
```
77+
info: revealed-type: Revealed type
78+
--> src/mdtest_snippet.py:9:1
79+
|
80+
7 | reveal_type(f(True)) # revealed: Literal[True]
81+
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
82+
9 | reveal_type(f("string")) # revealed: Unknown
83+
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
84+
|
85+
86+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
source: crates/red_knot_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: functions.md - Generic functions - Inferring a constrained typevar
7+
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing_extensions import reveal_type
16+
2 |
17+
3 | def f[T: (int, None)](x: T) -> T:
18+
4 | return x
19+
5 |
20+
6 | reveal_type(f(1)) # revealed: int
21+
7 | reveal_type(f(True)) # revealed: int
22+
8 | reveal_type(f(None)) # revealed: None
23+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
24+
10 | reveal_type(f("string")) # revealed: Unknown
25+
```
26+
27+
# Diagnostics
28+
29+
```
30+
info: revealed-type: Revealed type
31+
--> src/mdtest_snippet.py:6:1
32+
|
33+
4 | return x
34+
5 |
35+
6 | reveal_type(f(1)) # revealed: int
36+
| ^^^^^^^^^^^^^^^^^ `int`
37+
7 | reveal_type(f(True)) # revealed: int
38+
8 | reveal_type(f(None)) # revealed: None
39+
|
40+
41+
```
42+
43+
```
44+
info: revealed-type: Revealed type
45+
--> src/mdtest_snippet.py:7:1
46+
|
47+
6 | reveal_type(f(1)) # revealed: int
48+
7 | reveal_type(f(True)) # revealed: int
49+
| ^^^^^^^^^^^^^^^^^^^^ `int`
50+
8 | reveal_type(f(None)) # revealed: None
51+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
52+
|
53+
54+
```
55+
56+
```
57+
info: revealed-type: Revealed type
58+
--> src/mdtest_snippet.py:8:1
59+
|
60+
6 | reveal_type(f(1)) # revealed: int
61+
7 | reveal_type(f(True)) # revealed: int
62+
8 | reveal_type(f(None)) # revealed: None
63+
| ^^^^^^^^^^^^^^^^^^^^ `None`
64+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
65+
10 | reveal_type(f("string")) # revealed: Unknown
66+
|
67+
68+
```
69+
70+
```
71+
error: lint:invalid-argument-type: Argument to this function is incorrect
72+
--> src/mdtest_snippet.py:10:15
73+
|
74+
8 | reveal_type(f(None)) # revealed: None
75+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
76+
10 | reveal_type(f("string")) # revealed: Unknown
77+
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T`
78+
|
79+
info: Type variable defined here
80+
--> src/mdtest_snippet.py:3:7
81+
|
82+
1 | from typing_extensions import reveal_type
83+
2 |
84+
3 | def f[T: (int, None)](x: T) -> T:
85+
| ^^^^^^^^^^^^^^
86+
4 | return x
87+
|
88+
89+
```
90+
91+
```
92+
info: revealed-type: Revealed type
93+
--> src/mdtest_snippet.py:10:1
94+
|
95+
8 | reveal_type(f(None)) # revealed: None
96+
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
97+
10 | reveal_type(f("string")) # revealed: Unknown
98+
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
99+
|
100+
101+
```

crates/red_knot_python_semantic/src/types/call/bind.rs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::types::diagnostic::{
1616
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
1717
UNKNOWN_ARGUMENT,
1818
};
19-
use crate::types::generics::{Specialization, SpecializationBuilder};
19+
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
2020
use crate::types::signatures::{Parameter, ParameterForm};
2121
use crate::types::{
2222
todo_type, BoundMethodType, DataclassParams, DataclassTransformerParams, FunctionDecorators,
@@ -1172,12 +1172,28 @@ impl<'db> Binding<'db> {
11721172
signature: &Signature<'db>,
11731173
argument_types: &CallArgumentTypes<'_, 'db>,
11741174
) {
1175+
let mut num_synthetic_args = 0;
1176+
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
1177+
if argument_index >= num_synthetic_args {
1178+
// Adjust the argument index to skip synthetic args, which don't appear at the call
1179+
// site and thus won't be in the Call node arguments list.
1180+
Some(argument_index - num_synthetic_args)
1181+
} else {
1182+
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
1183+
// entire Call node, since there's no argument node for this argument at the call site
1184+
None
1185+
}
1186+
};
1187+
11751188
// If this overload is generic, first see if we can infer a specialization of the function
11761189
// from the arguments that were passed in.
11771190
let parameters = signature.parameters();
11781191
if signature.generic_context.is_some() || signature.inherited_generic_context.is_some() {
11791192
let mut builder = SpecializationBuilder::new(db);
1180-
for (argument_index, (_, argument_type)) in argument_types.iter().enumerate() {
1193+
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() {
1194+
if matches!(argument, Argument::Synthetic) {
1195+
num_synthetic_args += 1;
1196+
}
11811197
let Some(parameter_index) = self.argument_parameters[argument_index] else {
11821198
// There was an error with argument when matching parameters, so don't bother
11831199
// type-checking it.
@@ -1187,26 +1203,20 @@ impl<'db> Binding<'db> {
11871203
let Some(expected_type) = parameter.annotated_type() else {
11881204
continue;
11891205
};
1190-
builder.infer(expected_type, argument_type);
1206+
if let Err(error) = builder.infer(expected_type, argument_type) {
1207+
self.errors.push(BindingError::SpecializationError {
1208+
error,
1209+
argument_index: get_argument_index(argument_index, num_synthetic_args),
1210+
});
1211+
}
11911212
}
11921213
self.specialization = signature.generic_context.map(|gc| builder.build(gc));
11931214
self.inherited_specialization = signature
11941215
.inherited_generic_context
11951216
.map(|gc| builder.build(gc));
11961217
}
11971218

1198-
let mut num_synthetic_args = 0;
1199-
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
1200-
if argument_index >= num_synthetic_args {
1201-
// Adjust the argument index to skip synthetic args, which don't appear at the call
1202-
// site and thus won't be in the Call node arguments list.
1203-
Some(argument_index - num_synthetic_args)
1204-
} else {
1205-
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
1206-
// entire Call node, since there's no argument node for this argument at the call site
1207-
None
1208-
}
1209-
};
1219+
num_synthetic_args = 0;
12101220
for (argument_index, (argument, argument_type)) in argument_types.iter().enumerate() {
12111221
if matches!(argument, Argument::Synthetic) {
12121222
num_synthetic_args += 1;
@@ -1434,6 +1444,11 @@ pub(crate) enum BindingError<'db> {
14341444
argument_index: Option<usize>,
14351445
parameter: ParameterContext,
14361446
},
1447+
/// An inferred specialization was invalid.
1448+
SpecializationError {
1449+
error: SpecializationError<'db>,
1450+
argument_index: Option<usize>,
1451+
},
14371452
/// The call itself might be well constructed, but an error occurred while evaluating the call.
14381453
/// We use this variant to report errors in `property.__get__` and `property.__set__`, which
14391454
/// can occur when the call to the underlying getter/setter fails.
@@ -1546,6 +1561,35 @@ impl<'db> BindingError<'db> {
15461561
}
15471562
}
15481563

1564+
Self::SpecializationError {
1565+
error,
1566+
argument_index,
1567+
} => {
1568+
let range = Self::get_node(node, *argument_index);
1569+
let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else {
1570+
return;
1571+
};
1572+
1573+
let typevar = error.typevar();
1574+
let argument_type = error.argument_type();
1575+
let argument_ty_display = argument_type.display(context.db());
1576+
1577+
let mut diag = builder.into_diagnostic("Argument to this function is incorrect");
1578+
diag.set_primary_message(format_args!(
1579+
"Argument type `{argument_ty_display}` does not satisfy {} of type variable `{}`",
1580+
match error {
1581+
SpecializationError::MismatchedBound {..} => "upper bound",
1582+
SpecializationError::MismatchedConstraint {..} => "constraints",
1583+
},
1584+
typevar.name(context.db()),
1585+
));
1586+
1587+
let typevar_range = typevar.definition(context.db()).full_range(context.db());
1588+
let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here");
1589+
sub.annotate(Annotation::primary(typevar_range.into()));
1590+
diag.sub(sub);
1591+
}
1592+
15491593
Self::InternalCallError(reason) => {
15501594
let node = Self::get_node(node, None);
15511595
if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) {

0 commit comments

Comments
 (0)