allow looking up types of identifiers

This commit is contained in:
Aria Desires 2025-12-02 12:48:55 -05:00
parent 05d053376b
commit 7ca384d0de
4 changed files with 312 additions and 24 deletions

View File

@ -334,15 +334,24 @@ impl GotoTarget<'_> {
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
ty
}
// TODO: Support identifier targets
GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_)
| GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. }
| GotoTarget::Globals { .. } => return None,
GotoTarget::PatternMatchRest(pattern) => {
model.inferred_type_for_identifier(pattern.rest.as_ref()?)
}
GotoTarget::PatternKeywordArgument(pattern) => {
model.inferred_type_for_identifier(&pattern.attr)
}
GotoTarget::PatternMatchStarName(pattern) => {
model.inferred_type_for_identifier(pattern.name.as_ref()?)
}
GotoTarget::PatternMatchAsName(pattern) => {
model.inferred_type_for_identifier(pattern.name.as_ref()?)
}
GotoTarget::NonLocal { identifier } => model.inferred_type_for_identifier(identifier),
GotoTarget::Globals { identifier } => model.inferred_type_for_identifier(identifier),
// These don't really... *have* a type?
GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) => {
return None;
}
};
Some(ty)

View File

@ -975,7 +975,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@ -1003,7 +1022,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@ -1031,7 +1069,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^^
5 | x = ab
|
"#);
}
#[test]
@ -1065,7 +1122,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^
11 | x = ab
|
");
}
#[test]
@ -1143,7 +1219,26 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:10:23
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^^^^
11 | x = ab
|
");
}
#[test]
@ -1395,7 +1490,26 @@ def outer():
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:6:18
|
5 | def inner():
6 | nonlocal xy
| ^^
7 | xy = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
|
"#);
}
#[test]
@ -1447,7 +1561,26 @@ def function():
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:20:1
|
19 | # Types
20 | Unknown = object()
| ^^^^^^^
21 | AlwaysTruthy = object()
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:5:12
|
4 | def function():
5 | global global_var
| ^^^^^^^^^^
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
|
"#);
}
#[test]

View File

@ -1704,7 +1704,26 @@ def outer():
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:6:18
|
5 | def inner():
6 | nonlocal xy
| ^-
| ||
| |Cursor offset
| source
7 | xy = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
|
"#);
}
#[test]
@ -1756,7 +1775,26 @@ def function():
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:12
|
4 | def function():
5 | global global_var
| ^^^^^^^-^^
| | |
| | Cursor offset
| source
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
|
"#);
}
#[test]
@ -1770,7 +1808,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:22
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@ -1816,7 +1873,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:23
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", *ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@ -1862,7 +1938,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r#"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:37
|
2 | def my_func(command: str):
3 | match command.split():
4 | case ["get", ("a" | "b") as ab]:
| ^-
| ||
| |Cursor offset
| source
5 | x = ab
|
"#);
}
#[test]
@ -1914,7 +2009,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:30
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^-
| ||
| |Cursor offset
| source
11 | x = ab
|
");
}
#[test]
@ -2011,7 +2125,26 @@ def function():
"#,
);
assert_snapshot!(test.hover(), @"Hover provided no content");
assert_snapshot!(test.hover(), @r"
Unknown
---------------------------------------------
```python
Unknown
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:23
|
8 | def my_func(event: Click):
9 | match event:
10 | case Click(x, button=ab):
| ^^^-^^
| | |
| | Cursor offset
| source
11 | x = ab
|
");
}
#[test]

View File

@ -1,6 +1,6 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::{line_index, source_text};
use ruff_python_ast::{self as ast, ExprStringLiteral, ModExpression};
use ruff_python_ast::{self as ast, ExprStringLiteral, Identifier, ModExpression};
use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name};
use ruff_python_parser::Parsed;
use ruff_source_file::LineIndex;
@ -90,6 +90,19 @@ impl<'db> SemanticModel<'db> {
members
}
pub fn inferred_type_for_identifier(&self, identifier: &Identifier) -> Type<'db> {
// TODO(#1637): semantic tokens is making this crash even with
// `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`.
// The problematic input is `x: "float` (with a dangling quote). I imagine the issue
// is we're too eagerly setting `is_string_annotation` in inference.
let Some(file_scope) = self.scope(identifier.into()) else {
return Type::unknown();
};
let scope = file_scope.to_scope_id(self.db, self.file);
infer_scope_types(self.db, scope).expression_type(identifier)
}
/// Resolve the given import made in this file to a Type
pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option<Type<'db>> {
let module = self.resolve_module(module, level)?;