Goto definition, declaration, find references for typed dict and named tuple initializers (#24897)

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>
This commit is contained in:
Micha Reiser
2026-04-28 17:46:15 +02:00
committed by GitHub
parent 59c6c1f1a8
commit dfba807cc8
13 changed files with 835 additions and 53 deletions
+113
View File
@@ -1170,6 +1170,119 @@ instance = ExampleClass(old_name="test")
"#);
}
#[test]
fn references_keyword_argument_typeddict_field() {
let test = cursor_test(
"
from typing import TypedDict
class TD(TypedDict):
f<CURSOR>: int
g: str
TD(f=1)
",
);
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> main.py:5:5
|
5 | f: int
| -
6 | g: str
7 |
8 | TD(f=1)
| -
|
");
}
#[test]
fn references_typeddict_field_from_keyword_argument() {
let test = cursor_test(
"
from typing import TypedDict
class TD(TypedDict):
f: int
g: str
TD(f<CURSOR>=1)
",
);
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> main.py:5:5
|
5 | f: int
| -
6 | g: str
7 |
8 | TD(f=1)
| -
|
");
}
#[test]
fn references_keyword_argument_namedtuple_field() {
let test = cursor_test(
"
from typing import NamedTuple
class NT(NamedTuple):
f<CURSOR>: int
g: str
NT(f=1)
",
);
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> main.py:5:5
|
5 | f: int
| -
6 | g: str
7 |
8 | NT(f=1)
| -
|
");
}
#[test]
fn references_keyword_argument_dataclass_field() {
let test = cursor_test(
"
from dataclasses import dataclass
@dataclass
class DC:
f<CURSOR>: int
g: str
DC(f=1)
",
);
assert_snapshot!(test.references(), @"
info[references]: Found 2 references
--> main.py:6:5
|
6 | f: int
| -
7 | g: str
8 |
9 | DC(f=1)
| -
|
");
}
#[test]
fn multi_file_function_parameter_references_include_keyword_argument_labels() {
let test = CursorTest::builder()
+158
View File
@@ -2147,6 +2147,164 @@ class MyClass:
"#);
}
#[test]
fn goto_declaration_keyword_argument_typeddict() {
let test = cursor_test(
r#"
from typing import TypedDict
class TD(TypedDict):
f: int
g: str
TD(f<CURSOR>=1)
"#,
);
assert_snapshot!(test.goto_declaration(), @"
info[goto-declaration]: Go to declaration
--> main.py:8:4
|
8 | TD(f=1)
| ^ Clicking here
|
info: Found 1 declaration
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_declaration_keyword_argument_namedtuple() {
let test = cursor_test(
r#"
from typing import NamedTuple
class NT(NamedTuple):
f: int
g: str
NT(f<CURSOR>=1)
"#,
);
assert_snapshot!(test.goto_declaration(), @"
info[goto-declaration]: Go to declaration
--> main.py:8:4
|
8 | NT(f=1)
| ^ Clicking here
|
info: Found 1 declaration
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_declaration_keyword_argument_dataclass() {
let test = cursor_test(
r#"
from dataclasses import dataclass
@dataclass
class DC:
f: int
g: str
DC(f<CURSOR>=1)
"#,
);
assert_snapshot!(test.goto_declaration(), @"
info[goto-declaration]: Go to declaration
--> main.py:9:4
|
9 | DC(f=1)
| ^ Clicking here
|
info: Found 1 declaration
--> main.py:6:5
|
6 | f: int
| -
|
");
}
#[test]
fn goto_declaration_keyword_argument_dataclass_custom_init() {
let test = cursor_test(
r#"
from dataclasses import dataclass
@dataclass
class DC:
f: int
g: str
def __init__(self, f: int) -> None: ...
DC(f<CURSOR>=1)
"#,
);
assert_snapshot!(test.goto_declaration(), @"
info[goto-declaration]: Go to declaration
--> main.py:11:4
|
11 | DC(f=1)
| ^ Clicking here
|
info: Found 1 declaration
--> main.py:9:24
|
9 | def __init__(self, f: int) -> None: ...
| -
|
");
}
#[test]
fn goto_declaration_keyword_argument_dataclass_transform_alias() {
let test = cursor_test(
r#"
from typing import dataclass_transform
def Field(alias: str = ...): ...
@dataclass_transform(field_specifiers=(Field,))
class MyDataclass: ...
class DC(MyDataclass):
f: int = Field(alias='g')
DC(g<CURSOR>=1)
"#,
);
assert_snapshot!(test.goto_declaration(), @"
info[goto-declaration]: Go to declaration
--> main.py:12:4
|
12 | DC(g=1)
| ^ Clicking here
|
info: Found 1 declaration
--> main.py:10:5
|
10 | f: int = Field(alias='g')
| -
|
");
}
#[test]
fn goto_declaration_overload_type_disambiguated1() {
let test = CursorTest::builder()
+242
View File
@@ -2090,6 +2090,248 @@ while True:
");
}
#[test]
fn goto_definition_keyword_argument_typeddict() {
let test = CursorTest::builder()
.source(
"main.py",
"
from typing import TypedDict
class TD(TypedDict):
f: int
g: str
TD(f<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:8:4
|
8 | TD(f=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_typeddict_update() {
let test = CursorTest::builder()
.source(
"main.py",
"
from typing import TypedDict
class TD(TypedDict):
f: int
g: str
td = TD(f=1, g=\"\")
td.update(f<CURSOR>=2)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:9:11
|
9 | td.update(f=2)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_unpack_typeddict() {
let test = CursorTest::builder()
.source(
"main.py",
"
from typing import TypedDict, Unpack
class TD(TypedDict):
f: int
g: str
def func(**kwargs: Unpack[TD]): ...
func(f<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:10:6
|
10 | func(f=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_namedtuple() {
let test = CursorTest::builder()
.source(
"main.py",
"
from typing import NamedTuple
class NT(NamedTuple):
f: int
g: str
NT(f<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:8:4
|
8 | NT(f=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:5:5
|
5 | f: int
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_dataclass() {
let test = CursorTest::builder()
.source(
"main.py",
"
from dataclasses import dataclass
@dataclass
class DC:
f: int
g: str
DC(f<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:9:4
|
9 | DC(f=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:6:5
|
6 | f: int
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_dataclass_custom_init() {
let test = CursorTest::builder()
.source(
"main.py",
"
from dataclasses import dataclass
@dataclass
class DC:
f: int
g: str
def __init__(self, f: int) -> None: ...
DC(f<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:11:4
|
11 | DC(f=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:9:24
|
9 | def __init__(self, f: int) -> None: ...
| -
|
");
}
#[test]
fn goto_definition_keyword_argument_dataclass_transform_alias() {
let test = CursorTest::builder()
.source(
"main.py",
"
from typing import dataclass_transform
def Field(alias: str = ...): ...
@dataclass_transform(field_specifiers=(Field,))
class MyDataclass: ...
class DC(MyDataclass):
f: int = Field(alias='g')
DC(g<CURSOR>=1)
",
)
.build();
assert_snapshot!(test.goto_definition(), @"
info[goto-definition]: Go to definition
--> main.py:12:4
|
12 | DC(g=1)
| ^ Clicking here
|
info: Found 1 definition
--> main.py:10:5
|
10 | f: int = Field(alias='g')
| -
|
");
}
/// Go-to-definition should not point to for-loop header definitions.
#[test]
fn goto_definition_does_not_point_to_for_loop_header() {
+73
View File
@@ -4317,6 +4317,66 @@ Source with applied edits:
");
}
#[test]
fn test_named_tuple_constructor_call() {
let mut test = inlay_hint_test(
"
from typing import NamedTuple
class Foo(NamedTuple):
x: int
y: str
Foo(1, 'a')",
);
assert_snapshot!(test.inlay_hints(), @"
from typing import NamedTuple
class Foo(NamedTuple):
x: int
y: str
Foo([x=]1, [y=]'a')
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:5:5
|
5 | x: int
| ^
|
info: Source
--> main2.py:8:6
|
8 | Foo([x=]1, [y=]'a')
| ^
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:6:5
|
6 | y: str
| ^
|
info: Source
--> main2.py:8:13
|
8 | Foo([x=]1, [y=]'a')
| ^
|
---------------------------------------------
info[inlay-hint-edit]: Inlay hint edits
--> main.py:1:1
5 | x: int
6 | y: str
7 |
- Foo(1, 'a')
8 + Foo(1, y='a')
");
}
#[test]
fn test_class_constructor_call_new() {
let mut test = inlay_hint_test(
@@ -7572,6 +7632,19 @@ Source with applied edits:
| ^
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:9:5
|
9 | x: T
| ^
|
info: Source
--> main2.py:11:16
|
11 | b[: B[A]] = B([x=]foo.A())
| ^
|
---------------------------------------------
info[inlay-hint-edit]: Inlay hint edits
--> main.py:1:1
+113
View File
@@ -1190,6 +1190,119 @@ result = func(10, y=20)
");
}
#[test]
fn rename_keyword_argument_typeddict_field() {
let test = cursor_test(
"
from typing import TypedDict
class TD(TypedDict):
f<CURSOR>: int
g: str
TD(f=1)
",
);
assert_snapshot!(test.rename("z"), @"
info[rename]: Rename symbol (found 2 locations)
--> main.py:5:5
|
5 | f: int
| ^
6 | g: str
7 |
8 | TD(f=1)
| -
|
");
}
#[test]
fn rename_typeddict_field_from_keyword_argument() {
let test = cursor_test(
"
from typing import TypedDict
class TD(TypedDict):
f: int
g: str
TD(f<CURSOR>=1)
",
);
assert_snapshot!(test.rename("z"), @"
info[rename]: Rename symbol (found 2 locations)
--> main.py:5:5
|
5 | f: int
| ^
6 | g: str
7 |
8 | TD(f=1)
| -
|
");
}
#[test]
fn rename_keyword_argument_namedtuple_field() {
let test = cursor_test(
"
from typing import NamedTuple
class NT(NamedTuple):
f<CURSOR>: int
g: str
NT(f=1)
",
);
assert_snapshot!(test.rename("z"), @"
info[rename]: Rename symbol (found 2 locations)
--> main.py:5:5
|
5 | f: int
| ^
6 | g: str
7 |
8 | NT(f=1)
| -
|
");
}
#[test]
fn rename_keyword_argument_dataclass_field() {
let test = cursor_test(
"
from dataclasses import dataclass
@dataclass
class DC:
f<CURSOR>: int
g: str
DC(f=1)
",
);
assert_snapshot!(test.rename("z"), @"
info[rename]: Rename symbol (found 2 locations)
--> main.py:6:5
|
6 | f: int
| ^
7 | g: str
8 |
9 | DC(f=1)
| -
|
");
}
#[test]
fn import_alias() {
let test = CursorTest::builder()
@@ -52,6 +52,7 @@ pub(super) fn synthesize_namedtuple_class_member<'db>(
Parameter::positional_or_keyword(field.name)
.with_annotated_type(field.ty)
.with_optional_default_type(field.default)
.with_definition(field.definition)
}));
let signature = Signature::new_generic(
@@ -89,6 +90,7 @@ pub(super) fn synthesize_namedtuple_class_member<'db>(
Parameter::keyword_only(field.name)
.with_annotated_type(field.ty)
.with_default_type(field.ty)
.with_definition(field.definition)
}));
let signature = Signature::new(Parameters::new(db, parameters), self_ty);
@@ -115,6 +117,8 @@ pub struct NamedTupleField<'db> {
pub(crate) name: Name,
pub(crate) ty: Type<'db>,
pub(crate) default: Option<Type<'db>>,
/// The field's first declaration for a class based named tuple.
pub(crate) definition: Option<Definition<'db>>,
}
/// A namedtuple created via the functional form `namedtuple(name, fields)` or
@@ -558,6 +562,7 @@ impl<'db> NamedTupleSpec<'db> {
.unwrap_or(div)
},
default: None,
definition: f.definition,
})
})
.collect::<Option<Box<_>>>()?;
@@ -1293,7 +1293,8 @@ impl<'db> StaticClassLiteral<'db> {
} else {
Parameter::positional_or_keyword(parameter_name)
}
.with_annotated_type(field_ty);
.with_annotated_type(field_ty)
.with_definition(field.first_declaration);
parameter = if matches!(name, "__replace__" | "_replace") {
// When replacing, we know there is a default value for the field
@@ -1369,6 +1370,7 @@ impl<'db> StaticClassLiteral<'db> {
name: name.clone(),
ty: field.declared_ty,
default: default_ty,
definition: field.first_declaration,
}
});
synthesize_namedtuple_class_member(
@@ -114,6 +114,7 @@ fn synthesize_typed_dict_init<'db>(
Parameter::keyword_only((*name).clone())
.with_annotated_type(field.declared_ty)
.with_default_type(field.declared_ty)
.with_definition(field.first_declaration())
});
let map_overload = Signature::new(
@@ -128,7 +129,9 @@ fn synthesize_typed_dict_init<'db>(
);
let keyword_field_params = keyword_fields.iter().map(|(name, field)| {
let param = Parameter::keyword_only((*name).clone()).with_annotated_type(field.declared_ty);
let param = Parameter::keyword_only((*name).clone())
.with_annotated_type(field.declared_ty)
.with_definition(field.first_declaration());
if field.is_required() {
param
} else {
@@ -392,6 +395,7 @@ fn synthesize_typed_dict_update<'db>(
Parameter::keyword_only(field_name.clone())
.with_annotated_type(ty)
.with_default_type(ty)
.with_definition(field.first_declaration())
});
let update_patch_ty = if let Type::TypedDict(typed_dict) = instance_ty {
@@ -500,26 +500,11 @@ pub fn definitions_for_keyword_argument<'db>(
// For each signature, find the parameter with the matching name
for signature in signatures {
if let Some((_param_index, _param)) =
if let Some((_param_index, param)) =
signature.parameters().keyword_by_name(keyword_name_str)
&& let Some(definition) = param.definition()
{
if let Some(function_definition) = signature.definition() {
let function_file = function_definition.file(db);
let module = parsed_module(db, function_file).load(db);
let def_kind = function_definition.kind(db);
if let DefinitionKind::Function(function_ast_ref) = def_kind {
let function_node = function_ast_ref.node(&module);
if let Some(parameter_range) =
find_parameter_range(&function_node.parameters, keyword_name_str)
{
resolved_definitions.push(ResolvedDefinition::FileWithRange(
FileRange::new(function_file, parameter_range),
));
}
}
}
resolved_definitions.push(ResolvedDefinition::Definition(definition));
}
}
}
@@ -648,20 +633,6 @@ impl<'db> CallSignatureDetails<'db> {
argument_to_displayed_parameter_mapping,
}
}
fn get_definition_parameter_range(&self, db: &dyn Db, name: &str) -> Option<FileRange> {
let definition = self.signature.definition()?;
let file = definition.file(db);
let module_ref = parsed_module(db, file).load(db);
let parameters = match definition.kind(db) {
DefinitionKind::Function(node) => &node.node(&module_ref).parameters,
// TODO: lambda functions
_ => return None,
};
Some(FileRange::new(file, parameters.find(name)?.name().range))
}
}
/// Build the parameter list shown for a rendered signature.
@@ -1258,7 +1229,11 @@ pub fn inlay_hint_call_argument_details<'db>(
continue;
};
let parameter_label_offset = resolved.get_definition_parameter_range(db, param.name()?);
let parameter_label_offset = param.definition().map(|definition| {
let param_file = definition.file(db);
let module = parsed_module(db, param_file).load(db);
definition.focus_range(db, &module)
});
// Only add hints for parameters that can be specified by name
if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() {
@@ -1272,18 +1247,6 @@ pub fn inlay_hint_call_argument_details<'db>(
Some(InlayHintCallArgumentDetails { argument_names })
}
/// Find the text range of a specific parameter in function parameters by name.
/// Only searches for parameters that can be addressed by name in keyword arguments.
fn find_parameter_range(parameters: &ast::Parameters, parameter_name: &str) -> Option<TextRange> {
// Check regular positional and keyword-only parameters
parameters
.args
.iter()
.chain(&parameters.kwonlyargs)
.find(|param| param.parameter.name.as_str() == parameter_name)
.map(|param| param.parameter.name.range())
}
mod resolve_definition {
//! Resolves an Import, `ImportFrom` or `StarImport` definition to one or more
//! "resolved definitions". This is done recursively to find the original
@@ -181,7 +181,7 @@ const NUM_FIELD_SPECIFIERS_INLINE: usize = 1;
///
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
/// [`finish_expression()`](TypeInferenceBuilder::finish_expression), [`finish_definition()`](TypeInferenceBuilder::finish_definition), or [`finish_scope()`](TypeInferenceBuilder::finish_scope) on it, which returns
/// type inference result..
/// type inference result.
///
/// There are a few different kinds of methods in the type inference builder, and the naming
/// distinctions are a bit subtle.
@@ -533,6 +533,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
name: field_name.clone(),
ty: Type::any(),
default,
definition: None,
}
})
.collect();
@@ -672,6 +673,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
name: Name::new(name.value(db)),
ty: declared_type,
default: None,
definition: None,
};
fields.push(field);
@@ -2564,7 +2564,8 @@ impl<'db> Parameters<'db> {
.with_annotated_type(unpacked_key.value_ty)
.with_optional_default_type(
(!unpacked_key.is_required).then_some(Type::unknown()),
),
)
.with_definition(unpacked_key.definition),
);
}
@@ -3088,6 +3089,14 @@ pub(crate) struct Parameter<'db> {
/// Annotated type of the parameter. If no annotation was provided, this is `Unknown`.
annotated_type: Type<'db>,
/// The source definition represented by this parameter, if any.
///
/// For source-backed signatures, this is the definition of the parameter itself. For
/// synthesized signatures, this can point to the field or declaration that the synthesized
/// parameter represents, such as a dataclass field or `TypedDict` item. IDE features use this to
/// navigate from keyword arguments back to the declaration that defines the accepted keyword.
definition: Option<Definition<'db>>,
/// Does the type of this parameter come from an explicit annotation, or was it inferred from
/// the context, like `Unknown` for any normal un-annotated parameter, `Self` for the `self`
/// parameter of instance method, or `type[Self]` for `cls` parameter of classmethods. This
@@ -3123,6 +3132,7 @@ impl<'db> Parameter<'db> {
pub(crate) fn positional_only(name: Option<Name>) -> Self {
Self {
annotated_type: Type::unknown(),
definition: None,
inferred_annotation: true,
annotation_kind: ParameterAnnotationKind::Normal,
kind: ParameterKind::PositionalOnly {
@@ -3136,6 +3146,7 @@ impl<'db> Parameter<'db> {
pub(crate) fn positional_or_keyword(name: Name) -> Self {
Self {
annotated_type: Type::unknown(),
definition: None,
inferred_annotation: true,
annotation_kind: ParameterAnnotationKind::Normal,
kind: ParameterKind::PositionalOrKeyword {
@@ -3149,6 +3160,7 @@ impl<'db> Parameter<'db> {
pub(crate) fn variadic(name: Name) -> Self {
Self {
annotated_type: Type::unknown(),
definition: None,
inferred_annotation: true,
annotation_kind: ParameterAnnotationKind::Normal,
kind: ParameterKind::Variadic { name },
@@ -3159,6 +3171,7 @@ impl<'db> Parameter<'db> {
pub(crate) fn keyword_only(name: Name) -> Self {
Self {
annotated_type: Type::unknown(),
definition: None,
inferred_annotation: true,
annotation_kind: ParameterAnnotationKind::Normal,
kind: ParameterKind::KeywordOnly {
@@ -3172,6 +3185,7 @@ impl<'db> Parameter<'db> {
pub(crate) fn keyword_variadic(name: Name) -> Self {
Self {
annotated_type: Type::unknown(),
definition: None,
inferred_annotation: true,
annotation_kind: ParameterAnnotationKind::Normal,
kind: ParameterKind::KeywordVariadic { name },
@@ -3207,6 +3221,12 @@ impl<'db> Parameter<'db> {
}
}
/// Set the source definition represented by this parameter.
pub(crate) fn with_definition(mut self, definition: Option<Definition<'db>>) -> Self {
self.definition = definition;
self
}
pub(crate) fn type_form(mut self) -> Self {
self.form = ParameterForm::Type;
self
@@ -3226,6 +3246,7 @@ impl<'db> Parameter<'db> {
tcx,
visitor,
),
definition: self.definition,
kind: self
.kind
.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
@@ -3244,6 +3265,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type,
definition: self.definition,
inferred_annotation: self.inferred_annotation,
annotation_kind: self.annotation_kind,
kind,
@@ -3259,6 +3281,7 @@ impl<'db> Parameter<'db> {
) -> Option<Self> {
let Parameter {
annotated_type,
definition,
annotation_kind,
inferred_annotation,
kind,
@@ -3319,6 +3342,7 @@ impl<'db> Parameter<'db> {
Some(Self {
annotated_type,
definition: *definition,
inferred_annotation: *inferred_annotation,
annotation_kind: *annotation_kind,
kind,
@@ -3328,14 +3352,17 @@ impl<'db> Parameter<'db> {
fn from_node_and_kind(
db: &'db dyn Db,
definition: Definition<'db>,
function_definition: Definition<'db>,
parameter: &ast::Parameter,
kind: ParameterKind<'db>,
) -> Self {
let index = semantic_index(db, function_definition.file(db));
let definition = Some(index.expect_single_definition(parameter));
let (annotated_type, inferred_annotation, has_starred_annotation) =
if let Some(annotation) = parameter.annotation() {
(
function_signature_expression_type(db, definition, annotation),
function_signature_expression_type(db, function_definition, annotation),
false,
annotation.is_starred_expr(),
)
@@ -3347,7 +3374,7 @@ impl<'db> Parameter<'db> {
extract_unpacked_typed_dict_keys_from_kwargs_annotation(
db,
annotated_type,
function_signature_type_expression_flags(db, definition, annotation),
function_signature_type_expression_flags(db, function_definition, annotation),
)
.is_some()
});
@@ -3360,6 +3387,7 @@ impl<'db> Parameter<'db> {
};
Self {
annotated_type,
definition,
kind,
annotation_kind,
form: ParameterForm::Value,
@@ -3437,6 +3465,11 @@ impl<'db> Parameter<'db> {
self.annotated_type
}
/// Returns the source definition represented by this parameter, if any.
pub(crate) fn definition(&self) -> Option<Definition<'db>> {
self.definition
}
/// Return `true` if this parameter has a starred annotation,
/// e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...], bytes]`
pub(crate) fn has_starred_annotation(&self) -> bool {
@@ -3638,7 +3671,58 @@ mod tests {
#[track_caller]
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
assert_eq!(signature.parameters.value.as_slice(), expected);
assert_eq!(
signature
.parameters
.value
.iter()
.map(ParameterWithoutDefinition::from)
.collect::<Vec<_>>(),
expected
.iter()
.map(ParameterWithoutDefinition::from)
.collect::<Vec<_>>(),
);
}
#[derive(Debug, Eq, PartialEq)]
struct ParameterWithoutDefinition<'a, 'db> {
annotated_type: &'a Type<'db>,
annotation_kind: ParameterAnnotationKind,
inferred_annotation: bool,
kind: &'a ParameterKind<'db>,
form: ParameterForm,
}
impl<'a, 'db> From<&'a Parameter<'db>> for ParameterWithoutDefinition<'a, 'db> {
fn from(parameter: &'a Parameter<'db>) -> Self {
let Parameter {
annotated_type,
definition: _,
annotation_kind,
inferred_annotation,
kind,
form,
} = parameter;
Self {
annotated_type,
annotation_kind: *annotation_kind,
inferred_annotation: *inferred_annotation,
kind,
form: *form,
}
}
}
#[track_caller]
fn assert_params_have_definitions(signature: &Signature<'_>) {
for parameter in &signature.parameters.value {
assert!(
parameter.definition().is_some(),
"source-backed parameter should have a definition"
);
}
}
#[test]
@@ -3677,6 +3761,7 @@ mod tests {
let sig = func.signature(&db);
assert_eq!(sig.return_ty.display(&db).to_string(), "bytes");
assert_params_have_definitions(&sig);
assert_params(
&sig,
&[
@@ -855,6 +855,7 @@ pub(super) fn validate_typed_dict_required_keys<'db, 'ast>(
pub(crate) struct UnpackedTypedDictKey<'db> {
pub(crate) value_ty: Type<'db>,
pub(crate) is_required: bool,
pub(crate) definition: Option<Definition<'db>>,
}
/// Extracts `TypedDict` keys, their value types, and whether they are required when an unpacked
@@ -881,6 +882,7 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
UnpackedTypedDictKey {
value_ty: field.declared_ty,
is_required: field.is_required(),
definition: field.first_declaration(),
},
)
})
@@ -915,6 +917,10 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
unpacked_key.value_ty,
);
existing.is_required |= unpacked_key.is_required;
existing.definition = merge_unpacked_key_definitions(
existing.definition,
unpacked_key.definition,
);
})
.or_insert(unpacked_key);
}
@@ -938,6 +944,7 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
for key in all_keys {
let mut value_ty = UnionBuilder::new(db);
let mut is_required = true;
let mut definition = None;
let mut saw_key = false;
for key_map in &key_maps {
@@ -945,8 +952,14 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
saw_key = true;
value_ty = value_ty.add(unpacked_key.value_ty);
is_required &= unpacked_key.is_required;
definition = Some(if let Some(definition) = definition {
merge_unpacked_key_definitions(definition, unpacked_key.definition)
} else {
unpacked_key.definition
});
} else {
is_required = false;
definition = Some(None);
}
}
@@ -956,6 +969,7 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
UnpackedTypedDictKey {
value_ty: value_ty.build(),
is_required,
definition: definition.flatten(),
},
);
}
@@ -997,6 +1011,13 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>(
}
}
fn merge_unpacked_key_definitions<'db>(
existing: Option<Definition<'db>>,
new: Option<Definition<'db>>,
) -> Option<Definition<'db>> {
if existing == new { existing } else { None }
}
/// Extracts unpacked `TypedDict` keys for a `**kwargs` annotation only when the annotation
/// explicitly uses `Unpack[...]`.
///
@@ -1021,6 +1042,7 @@ pub(crate) fn extract_unpacked_typed_dict_keys_from_kwargs_annotation<'db>(
UnpackedTypedDictKey {
value_ty: field.declared_ty,
is_required: field.is_required(),
definition: field.first_declaration(),
},
)
})