Skip to content

Commit d38f6fc

Browse files
authored
[red-knot] Add property tests for callable types (#17006)
## Summary Part of #15382, this PR adds property tests for callable types. Specifically, this PR updates the property tests to generate an arbitrary signature for a general callable type which includes: * Arbitrary combination of parameter kinds in the correct order * Arbitrary number of parameters * Arbitrary optional types for annotation and return type * Arbitrary parameter names (no duplicate names), optional for positional-only parameters ## Test Plan ``` QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable ``` Also, the commands in CI: https://github.com/astral-sh/ruff/blob/d72b4100a33ccb35047f86e900bede6310c7c119/.github/workflows/daily_property_tests.yaml#L47-L52
1 parent 6be0a50 commit d38f6fc

File tree

1 file changed

+157
-3
lines changed

1 file changed

+157
-3
lines changed

crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use crate::db::tests::TestDb;
22
use crate::symbol::{builtins_symbol, known_module_symbol};
33
use crate::types::{
4-
BoundMethodType, IntersectionBuilder, KnownClass, KnownInstanceType, SubclassOfType, TupleType,
5-
Type, UnionType,
4+
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, Parameter,
5+
Parameters, Signature, SubclassOfType, TupleType, Type, UnionType,
66
};
77
use crate::{Db, KnownModule};
8+
use hashbrown::HashSet;
89
use quickcheck::{Arbitrary, Gen};
10+
use ruff_python_ast::name::Name;
911

1012
/// A test representation of a type that can be transformed unambiguously into a real Type,
1113
/// given a db.
@@ -45,6 +47,59 @@ pub(crate) enum Ty {
4547
class: &'static str,
4648
method: &'static str,
4749
},
50+
Callable {
51+
params: CallableParams,
52+
returns: Option<Box<Ty>>,
53+
},
54+
}
55+
56+
#[derive(Debug, Clone, PartialEq)]
57+
pub(crate) enum CallableParams {
58+
GradualForm,
59+
List(Vec<Param>),
60+
}
61+
62+
impl CallableParams {
63+
pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> {
64+
match self {
65+
CallableParams::GradualForm => Parameters::gradual_form(),
66+
CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| {
67+
let mut parameter = match param.kind {
68+
ParamKind::PositionalOnly => Parameter::positional_only(param.name),
69+
ParamKind::PositionalOrKeyword => {
70+
Parameter::positional_or_keyword(param.name.unwrap())
71+
}
72+
ParamKind::Variadic => Parameter::variadic(param.name.unwrap()),
73+
ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()),
74+
ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()),
75+
};
76+
if let Some(annotated_ty) = param.annotated_ty {
77+
parameter = parameter.with_annotated_type(annotated_ty.into_type(db));
78+
}
79+
if let Some(default_ty) = param.default_ty {
80+
parameter = parameter.with_default_type(default_ty.into_type(db));
81+
}
82+
parameter
83+
})),
84+
}
85+
}
86+
}
87+
88+
#[derive(Debug, Clone, PartialEq)]
89+
pub(crate) struct Param {
90+
kind: ParamKind,
91+
name: Option<Name>,
92+
annotated_ty: Option<Ty>,
93+
default_ty: Option<Ty>,
94+
}
95+
96+
#[derive(Debug, Clone, Copy, PartialEq)]
97+
enum ParamKind {
98+
PositionalOnly,
99+
PositionalOrKeyword,
100+
Variadic,
101+
KeywordOnly,
102+
KeywordVariadic,
48103
}
49104

50105
#[salsa::tracked]
@@ -131,6 +186,13 @@ impl Ty {
131186

132187
create_bound_method(db, function, builtins_class)
133188
}
189+
Ty::Callable { params, returns } => Type::Callable(CallableType::new(
190+
db,
191+
Signature::new(
192+
params.into_parameters(db),
193+
returns.map(|ty| ty.into_type(db)),
194+
),
195+
)),
134196
}
135197
}
136198
}
@@ -205,7 +267,7 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
205267
if size == 0 {
206268
arbitrary_core_type(g)
207269
} else {
208-
match u32::arbitrary(g) % 4 {
270+
match u32::arbitrary(g) % 5 {
209271
0 => arbitrary_core_type(g),
210272
1 => Ty::Union(
211273
(0..*g.choose(&[2, 3]).unwrap())
@@ -225,11 +287,103 @@ fn arbitrary_type(g: &mut Gen, size: u32) -> Ty {
225287
.map(|_| arbitrary_type(g, size - 1))
226288
.collect(),
227289
},
290+
4 => Ty::Callable {
291+
params: match u32::arbitrary(g) % 2 {
292+
0 => CallableParams::GradualForm,
293+
1 => CallableParams::List(arbitrary_parameter_list(g, size)),
294+
_ => unreachable!(),
295+
},
296+
returns: arbitrary_optional_type(g, size - 1).map(Box::new),
297+
},
228298
_ => unreachable!(),
229299
}
230300
}
231301
}
232302

303+
fn arbitrary_parameter_list(g: &mut Gen, size: u32) -> Vec<Param> {
304+
let mut params: Vec<Param> = vec![];
305+
let mut used_names = HashSet::new();
306+
307+
// First, choose the number of parameters to generate.
308+
for _ in 0..*g.choose(&[0, 1, 2, 3, 4, 5]).unwrap() {
309+
// Next, choose the kind of parameters that can be generated based on the last parameter.
310+
let next_kind = match params.last().map(|p| p.kind) {
311+
None | Some(ParamKind::PositionalOnly) => *g
312+
.choose(&[
313+
ParamKind::PositionalOnly,
314+
ParamKind::PositionalOrKeyword,
315+
ParamKind::Variadic,
316+
ParamKind::KeywordOnly,
317+
ParamKind::KeywordVariadic,
318+
])
319+
.unwrap(),
320+
Some(ParamKind::PositionalOrKeyword) => *g
321+
.choose(&[
322+
ParamKind::PositionalOrKeyword,
323+
ParamKind::Variadic,
324+
ParamKind::KeywordOnly,
325+
ParamKind::KeywordVariadic,
326+
])
327+
.unwrap(),
328+
Some(ParamKind::Variadic | ParamKind::KeywordOnly) => *g
329+
.choose(&[ParamKind::KeywordOnly, ParamKind::KeywordVariadic])
330+
.unwrap(),
331+
Some(ParamKind::KeywordVariadic) => {
332+
// There can't be any other parameter kind after a keyword variadic parameter.
333+
break;
334+
}
335+
};
336+
337+
let name = loop {
338+
let name = if matches!(next_kind, ParamKind::PositionalOnly) {
339+
arbitrary_optional_name(g)
340+
} else {
341+
Some(arbitrary_name(g))
342+
};
343+
if let Some(name) = name {
344+
if used_names.insert(name.clone()) {
345+
break Some(name);
346+
}
347+
} else {
348+
break None;
349+
}
350+
};
351+
352+
params.push(Param {
353+
kind: next_kind,
354+
name,
355+
annotated_ty: arbitrary_optional_type(g, size),
356+
default_ty: if matches!(next_kind, ParamKind::Variadic | ParamKind::KeywordVariadic) {
357+
None
358+
} else {
359+
arbitrary_optional_type(g, size)
360+
},
361+
});
362+
}
363+
364+
params
365+
}
366+
367+
fn arbitrary_optional_type(g: &mut Gen, size: u32) -> Option<Ty> {
368+
match u32::arbitrary(g) % 2 {
369+
0 => None,
370+
1 => Some(arbitrary_type(g, size)),
371+
_ => unreachable!(),
372+
}
373+
}
374+
375+
fn arbitrary_name(g: &mut Gen) -> Name {
376+
Name::new(format!("n{}", u32::arbitrary(g) % 10))
377+
}
378+
379+
fn arbitrary_optional_name(g: &mut Gen) -> Option<Name> {
380+
match u32::arbitrary(g) % 2 {
381+
0 => None,
382+
1 => Some(arbitrary_name(g)),
383+
_ => unreachable!(),
384+
}
385+
}
386+
233387
impl Arbitrary for Ty {
234388
fn arbitrary(g: &mut Gen) -> Ty {
235389
const MAX_SIZE: u32 = 2;

0 commit comments

Comments
 (0)