diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 17df9f11d9..284adec091 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -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) diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index fe85f44095..bdf12eee17 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -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] diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 3b4b463ee2..134ca5d14a 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -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] diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index f9ac728c40..eca7f58225 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -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> { let module = self.resolve_module(module, level)?;