mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
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:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¶meters.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(),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user