From c534bfaf01671b8020edd440bc7f4f22070af583 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 28 Nov 2025 08:41:21 -0500 Subject: [PATCH] [ty] Implement patterns and typevars in the LSP (#21671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **This is the final goto-targets with missing goto-definition/declaration implementations! You can now theoretically click on all the user-defined names in all the syntax. 🎉** This adds: * goto definition/declaration on patterns/typevars * find-references/rename on patterns/typevars * fixes syntax highlighting of `*rest` patterns This notably *does not* add: * goto-type for patterns/typevars * hover for patterns/typevars (because that's just goto-type for names) Also I realized we were at the precipice of one of the great GotoTarget sins being resolved, and so I made import aliases also resolve to a ResolvedDefinition. This removes a ton of cruft and prevents further backsliding. Note however that import aliases are, in general, completely jacked up when it comes to find-references/renames (both before and after this PR). Previously you could try to rename an import alias and it just wouldn't do anything. With this change we instead refuse to even let you try to rename it. Sorting out why import aliases are jacked up is an ongoing thing I hope to handle in a followup. ## Test Plan You'll surely not regret checking in 86 snapshot tests --- crates/ty_ide/src/completion.rs | 6 +- crates/ty_ide/src/goto.rs | 201 +++---- crates/ty_ide/src/goto_declaration.rs | 480 +++++++++++++++ crates/ty_ide/src/goto_references.rs | 546 ++++++++++++++++++ crates/ty_ide/src/goto_type_definition.rs | 276 +++++++++ crates/ty_ide/src/hover.rs | 392 +++++++++++++ crates/ty_ide/src/inlay_hints.rs | 125 ++++ crates/ty_ide/src/references.rs | 14 + crates/ty_ide/src/rename.rs | 384 ++++++++++++ crates/ty_ide/src/semantic_tokens.rs | 11 + crates/ty_ide/src/signature_help.rs | 5 +- .../src/semantic_index/builder.rs | 2 + 12 files changed, 2329 insertions(+), 113 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 4387c09346..8b4c38beb4 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -17,7 +17,7 @@ use ty_python_semantic::{ use crate::docstring::Docstring; use crate::find_node::covering_node; -use crate::goto::DefinitionsOrTargets; +use crate::goto::Definitions; use crate::importer::{ImportRequest, Importer}; use crate::symbols::QueryPattern; use crate::{Db, all_symbols}; @@ -220,9 +220,7 @@ impl<'db> Completion<'db> { db: &'db dyn Db, semantic: SemanticCompletion<'db>, ) -> Completion<'db> { - let definition = semantic - .ty - .and_then(|ty| DefinitionsOrTargets::from_ty(db, ty)); + let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty)); let documentation = definition.and_then(|def| def.docstring(db)); let is_type_check_only = semantic.is_type_check_only(db); Completion { diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index ab589ba5c5..3b086b91fd 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -212,16 +212,9 @@ pub(crate) enum GotoTarget<'a> { /// The resolved definitions for a `GotoTarget` #[derive(Debug, Clone)] -pub(crate) enum DefinitionsOrTargets<'db> { - /// We computed actual Definitions we can do followup queries on. - Definitions(Vec>), - /// We directly computed a navigation. - /// - /// We can't get docs or usefully compute goto-definition for this. - Targets(crate::NavigationTargets), -} +pub(crate) struct Definitions<'db>(pub Vec>); -impl<'db> DefinitionsOrTargets<'db> { +impl<'db> Definitions<'db> { pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option { let ty_def = ty.definition(db)?; let resolved = match ty_def { @@ -237,7 +230,7 @@ impl<'db> DefinitionsOrTargets<'db> { ResolvedDefinition::Definition(definition) } }; - Some(DefinitionsOrTargets::Definitions(vec![resolved])) + Some(Definitions(vec![resolved])) } /// Get the "goto-declaration" interpretation of this definition @@ -247,12 +240,7 @@ impl<'db> DefinitionsOrTargets<'db> { self, db: &'db dyn ty_python_semantic::Db, ) -> Option { - match self { - DefinitionsOrTargets::Definitions(definitions) => { - definitions_to_navigation_targets(db, None, definitions) - } - DefinitionsOrTargets::Targets(targets) => Some(targets), - } + definitions_to_navigation_targets(db, None, self.0) } /// Get the "goto-definition" interpretation of this definition @@ -263,12 +251,7 @@ impl<'db> DefinitionsOrTargets<'db> { self, db: &'db dyn ty_python_semantic::Db, ) -> Option { - match self { - DefinitionsOrTargets::Definitions(definitions) => { - definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions) - } - DefinitionsOrTargets::Targets(targets) => Some(targets), - } + definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), self.0) } /// Get the docstring for this definition @@ -277,13 +260,7 @@ impl<'db> DefinitionsOrTargets<'db> { /// so this will check both the goto-declarations and goto-definitions (in that order) /// and return the first one found. pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option { - let definitions = match self { - DefinitionsOrTargets::Definitions(definitions) => definitions, - // Can't find docs for these - // (make more cases DefinitionOrTargets::Definitions to get more docs!) - DefinitionsOrTargets::Targets(_) => return None, - }; - for definition in &definitions { + for definition in &self.0 { // If we got a docstring from the original definition, use it if let Some(docstring) = definition.docstring(db) { return Some(Docstring::new(docstring)); @@ -296,7 +273,7 @@ impl<'db> DefinitionsOrTargets<'db> { let stub_mapper = StubMapper::new(db); // Try to find the corresponding implementation definition - for definition in stub_mapper.map_definitions(definitions) { + for definition in stub_mapper.map_definitions(self.0) { if let Some(docstring) = definition.docstring(db) { return Some(Docstring::new(docstring)); } @@ -399,36 +376,32 @@ impl GotoTarget<'_> { &self, model: &SemanticModel<'db>, alias_resolution: ImportAliasResolution, - ) -> Option> { - use crate::NavigationTarget; - match self { - GotoTarget::Expression(expression) => definitions_for_expression(model, *expression) - .map(DefinitionsOrTargets::Definitions), + ) -> Option> { + let definitions = match self { + GotoTarget::Expression(expression) => definitions_for_expression(model, *expression), // For already-defined symbols, they are their own definitions - GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(function.definition(model)), - ])), + GotoTarget::FunctionDef(function) => Some(vec![ResolvedDefinition::Definition( + function.definition(model), + )]), - GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(class.definition(model)), - ])), + GotoTarget::ClassDef(class) => Some(vec![ResolvedDefinition::Definition( + class.definition(model), + )]), - GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(parameter.definition(model)), - ])), + GotoTarget::Parameter(parameter) => Some(vec![ResolvedDefinition::Definition( + parameter.definition(model), + )]), // For import aliases (offset within 'y' or 'z' in "from x import y as z") GotoTarget::ImportSymbolAlias { alias, import_from, .. } => { let symbol_name = alias.name.as_str(); - Some(DefinitionsOrTargets::Definitions( - definitions_for_imported_symbol( - model, - import_from, - symbol_name, - alias_resolution, - ), + Some(definitions_for_imported_symbol( + model, + import_from, + symbol_name, + alias_resolution, )) } @@ -448,14 +421,9 @@ impl GotoTarget<'_> { if alias_resolution == ImportAliasResolution::ResolveAliases { definitions_for_module(model, Some(alias.name.as_str()), 0) } else { - let alias_range = alias.asname.as_ref().unwrap().range; - Some(DefinitionsOrTargets::Targets( - crate::NavigationTargets::single(NavigationTarget { - file: model.file(), - focus_range: alias_range, - full_range: alias.range(), - }), - )) + alias.asname.as_ref().map(|name| { + definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name)) + }) } } @@ -463,45 +431,44 @@ impl GotoTarget<'_> { GotoTarget::KeywordArgument { keyword, call_expression, - } => Some(DefinitionsOrTargets::Definitions( - definitions_for_keyword_argument(model, keyword, call_expression), + } => Some(definitions_for_keyword_argument( + model, + keyword, + call_expression, )), // For exception variables, they are their own definitions (like parameters) GotoTarget::ExceptVariable(except_handler) => { - Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(except_handler.definition(model)), - ])) + Some(vec![ResolvedDefinition::Definition( + except_handler.definition(model), + )]) } - // For pattern match rest variables, they are their own definitions + // Patterns are glorified assignments but we have to look them up by ident + // because they're not expressions GotoTarget::PatternMatchRest(pattern_mapping) => { - if let Some(rest_name) = &pattern_mapping.rest { - let range = rest_name.range; - Some(DefinitionsOrTargets::Targets( - crate::NavigationTargets::single(NavigationTarget::new( - model.file(), - range, - )), - )) - } else { - None - } + pattern_mapping.rest.as_ref().map(|name| { + definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name)) + }) } - // For pattern match as names, they are their own definitions - GotoTarget::PatternMatchAsName(pattern_as) => { - if let Some(name) = &pattern_as.name { - let range = name.range; - Some(DefinitionsOrTargets::Targets( - crate::NavigationTargets::single(NavigationTarget::new( - model.file(), - range, - )), - )) - } else { - None - } + GotoTarget::PatternMatchAsName(pattern_as) => pattern_as.name.as_ref().map(|name| { + definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name)) + }), + + GotoTarget::PatternKeywordArgument(pattern_keyword) => { + let name = &pattern_keyword.attr; + Some(definitions_for_name( + model, + name.as_str(), + AnyNodeRef::Identifier(name), + )) + } + + GotoTarget::PatternMatchStarName(pattern_star) => { + pattern_star.name.as_ref().map(|name| { + definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name)) + }) } // For callables, both the definition of the callable and the actual function impl are relevant. @@ -516,7 +483,7 @@ impl GotoTarget<'_> { if definitions.is_empty() { None } else { - Some(DefinitionsOrTargets::Definitions(definitions)) + Some(definitions) } } @@ -524,14 +491,14 @@ impl GotoTarget<'_> { let (definitions, _) = ty_python_semantic::definitions_for_bin_op(model, expression)?; - Some(DefinitionsOrTargets::Definitions(definitions)) + Some(definitions) } GotoTarget::UnaryOp { expression, .. } => { let (definitions, _) = ty_python_semantic::definitions_for_unary_op(model, expression)?; - Some(DefinitionsOrTargets::Definitions(definitions)) + Some(definitions) } // String annotations sub-expressions require us to recurse into the sub-AST @@ -545,23 +512,47 @@ impl GotoTarget<'_> { .node() .as_expr_ref()?; definitions_for_expression(&submodel, subexpr) - .map(DefinitionsOrTargets::Definitions) } + + // nonlocal and global are essentially loads, but again they're statements, + // so we need to look them up by ident GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => { - Some(DefinitionsOrTargets::Definitions(definitions_for_name( + Some(definitions_for_name( model, identifier.as_str(), AnyNodeRef::Identifier(identifier), - ))) + )) } - // TODO: implement these - GotoTarget::PatternKeywordArgument(..) - | GotoTarget::PatternMatchStarName(..) - | GotoTarget::TypeParamTypeVarName(..) - | GotoTarget::TypeParamParamSpecName(..) - | GotoTarget::TypeParamTypeVarTupleName(..) => None, - } + // These are declarations of sorts, but they're stmts and not exprs, so look up by ident. + GotoTarget::TypeParamTypeVarName(type_var) => { + let name = &type_var.name; + Some(definitions_for_name( + model, + name.as_str(), + AnyNodeRef::Identifier(name), + )) + } + + GotoTarget::TypeParamParamSpecName(name) => { + let name = &name.name; + Some(definitions_for_name( + model, + name.as_str(), + AnyNodeRef::Identifier(name), + )) + } + + GotoTarget::TypeParamTypeVarTupleName(name) => { + let name = &name.name; + Some(definitions_for_name( + model, + name.as_str(), + AnyNodeRef::Identifier(name), + )) + } + }; + definitions.map(Definitions) } /// Returns the text representation of this goto target. @@ -1050,12 +1041,10 @@ fn definitions_for_module<'db>( model: &SemanticModel<'db>, module: Option<&str>, level: u32, -) -> Option> { +) -> Option>> { let module = model.resolve_module(module, level)?; let file = module.file(model.db())?; - Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Module(file), - ])) + Some(vec![ResolvedDefinition::Module(file)]) } /// Helper function to extract module component information from a dotted module name diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 296a667f53..45efa4ae22 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -1397,6 +1397,486 @@ def function(): "); } + #[test] + fn goto_declaration_match_name_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | + info: Source + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | + "#); + } + + #[test] + fn goto_declaration_match_name_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | + info: Source + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn goto_declaration_match_rest_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | + info: Source + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | + "#); + } + + #[test] + fn goto_declaration_match_rest_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | + info: Source + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", *ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn goto_declaration_match_as_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | ^^ + 5 | x = ab + | + 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] + fn goto_declaration_match_as_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | ^^ + 5 | x = ab + | + info: Source + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn goto_declaration_match_keyword_stmt() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | + 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] + fn goto_declaration_match_keyword_binding() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | + info: Source + --> main.py:11:17 + | + 9 | match event: + 10 | case Click(x, button=ab): + 11 | x = ab + | ^^ + | + "); + } + + #[test] + fn goto_declaration_match_class_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:2:7 + | + 2 | class Click: + | ^^^^^ + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + info: Source + --> main.py:10:14 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^^^^ + 11 | x = ab + | + "#); + } + + #[test] + fn goto_declaration_match_class_field_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_declaration(), @"No goto target found"); + } + + #[test] + fn goto_declaration_typevar_name_stmt() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + info: Source + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_declaration_typevar_name_binding() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + info: Source + --> main.py:2:37 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_declaration_typevar_spec_stmt() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + info: Source + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_declaration_typevar_spec_binding() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + info: Source + --> main.py:3:43 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_declaration_typevar_tuple_stmt() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + info: Source + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + "); + } + + #[test] + fn goto_declaration_typevar_tuple_binding() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Declaration + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + info: Source + --> main.py:2:38 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + "); + } + #[test] fn goto_declaration_property_getter_setter() { let test = cursor_test( diff --git a/crates/ty_ide/src/goto_references.rs b/crates/ty_ide/src/goto_references.rs index e8be7b35c3..514ebfc75b 100644 --- a/crates/ty_ide/src/goto_references.rs +++ b/crates/ty_ide/src/goto_references.rs @@ -898,6 +898,552 @@ cls = MyClass assert_snapshot!(test.references(), @"No references found"); } + #[test] + fn references_match_name_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_name_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_rest_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", *ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_rest_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", *ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_as_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_as_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:4:37 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + | ^^ + 5 | x = ab + | + + info[references]: Reference 2 + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + 5 | x = ab + | ^^ + | + "#); + } + + #[test] + fn references_match_keyword_stmt() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | + + info[references]: Reference 2 + --> main.py:11:17 + | + 9 | match event: + 10 | case Click(x, button=ab): + 11 | x = ab + | ^^ + | + "); + } + + #[test] + fn references_match_keyword_binding() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | + + info[references]: Reference 2 + --> main.py:11:17 + | + 9 | match event: + 10 | case Click(x, button=ab): + 11 | x = ab + | ^^ + | + "); + } + + #[test] + fn references_match_class_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.references(), @r#" + info[references]: Reference 1 + --> main.py:2:7 + | + 2 | class Click: + | ^^^^^ + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + + info[references]: Reference 2 + --> main.py:8:20 + | + 6 | self.button: str = btn + 7 | + 8 | def my_func(event: Click): + | ^^^^^ + 9 | match event: + 10 | case Click(x, button=ab): + | + + info[references]: Reference 3 + --> main.py:10:14 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^^^^ + 11 | x = ab + | + "#); + } + + #[test] + fn references_match_class_field_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.references(), @"No references found"); + } + + #[test] + fn references_typevar_name_stmt() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:2:37 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:2:46 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn references_typevar_name_binding() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:2:37 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:2:46 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn references_typevar_spec_stmt() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:3:43 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:3:53 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + "); + } + + #[test] + fn references_typevar_spec_binding() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:3:43 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:3:53 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ + | + "); + } + + #[test] + fn references_typevar_tuple_stmt() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:2:38 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:2:50 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + "); + } + + #[test] + fn references_typevar_tuple_binding() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.references(), @r" + info[references]: Reference 1 + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + + info[references]: Reference 2 + --> main.py:2:38 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + + info[references]: Reference 3 + --> main.py:2:50 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ + | + "); + } + #[test] fn test_multi_file_function_references() { let test = CursorTest::builder() diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 3a8f80b643..fe85f44095 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -964,6 +964,282 @@ mod tests { assert_snapshot!(test.goto_type_definition(), @"No goto target found"); } + #[test] + fn goto_type_match_name_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_match_name_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_match_rest_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_match_rest_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_match_as_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_match_as_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_match_keyword_stmt() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_match_keyword_binding() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_match_class_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> main.py:2:7 + | + 2 | class Click: + | ^^^^^ + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + info: Source + --> main.py:10:14 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^^^^ + 11 | x = ab + | + "#); + } + + #[test] + fn goto_type_match_class_field_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_typevar_name_stmt() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + info: Source + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_type_typevar_name_binding() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + info: Source + --> main.py:2:37 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ + | + "); + } + + #[test] + fn goto_type_typevar_spec_stmt() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_typevar_spec_binding() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + + #[test] + fn goto_type_typevar_tuple_stmt() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_typevar_tuple_binding() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.goto_type_definition(), @"No type definitions found"); + } + #[test] fn goto_type_on_keyword_argument() { let test = cursor_test( diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 2f96f7b061..3b4b463ee2 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1759,6 +1759,398 @@ def function(): assert_snapshot!(test.hover(), @"Hover provided no content"); } + #[test] + fn hover_match_name_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_match_name_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @r#" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ab]: + 5 | x = ab + | ^- + | || + | |Cursor offset + | source + | + "#); + } + + #[test] + fn hover_match_rest_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_match_rest_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @r#" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", *ab]: + 5 | x = ab + | ^- + | || + | |Cursor offset + | source + | + "#); + } + + #[test] + fn hover_match_as_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_match_as_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @r#" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:5:17 + | + 3 | match command.split(): + 4 | case ["get", ("a" | "b") as ab]: + 5 | x = ab + | ^- + | || + | |Cursor offset + | source + | + "#); + } + + #[test] + fn hover_match_keyword_stmt() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_match_keyword_binding() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @r" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:11:17 + | + 9 | match event: + 10 | case Click(x, button=ab): + 11 | x = ab + | ^- + | || + | |Cursor offset + | source + | + "); + } + + #[test] + fn hover_match_class_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + ```python + + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:10:14 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^-^^ + | | | + | | Cursor offset + | source + 11 | x = ab + | + "); + } + + #[test] + fn hover_match_class_field_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_typevar_name_stmt() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.hover(), @r" + AB@Alias1 (invariant) + --------------------------------------------- + ```python + AB@Alias1 (invariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^- + | || + | |Cursor offset + | source + | + "); + } + + #[test] + fn hover_typevar_name_binding() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.hover(), @r" + AB@Alias1 (invariant) + --------------------------------------------- + ```python + AB@Alias1 (invariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:37 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^- + | || + | |Cursor offset + | source + | + "); + } + + #[test] + fn hover_typevar_spec_stmt() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_typevar_spec_binding() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.hover(), @r" + ( + ... + ) -> tuple[typing.ParamSpec] + --------------------------------------------- + ```python + ( + ... + ) -> tuple[typing.ParamSpec] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:43 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^- + | || + | |Cursor offset + | source + | + "); + } + + #[test] + fn hover_typevar_tuple_stmt() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_typevar_tuple_binding() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.hover(), @r" + @Todo + --------------------------------------------- + ```python + @Todo + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:2:38 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^- + | || + | |Cursor offset + | source + | + "); + } + #[test] fn hover_module_import() { let mut test = cursor_test( diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index a661ff1b9c..fea9b2030f 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1946,6 +1946,131 @@ mod tests { "#); } + #[test] + fn test_match_name_binding() { + let mut test = inlay_hint_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x[: @Todo] = ab + "#); + } + + #[test] + fn test_match_rest_binding() { + let mut test = inlay_hint_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x[: @Todo] = ab + "#); + } + + #[test] + fn test_match_as_binding() { + let mut test = inlay_hint_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x[: @Todo] = ab + "#); + } + + #[test] + fn test_match_keyword_binding() { + let mut test = inlay_hint_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.inlay_hints(), @r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x[: @Todo] = ab + "#); + } + + #[test] + fn test_typevar_name_binding() { + let mut test = inlay_hint_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.inlay_hints(), @"type Alias1[AB: int = bool] = tuple[AB, list[AB]]"); + } + + #[test] + fn test_typevar_spec_binding() { + let mut test = inlay_hint_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.inlay_hints(), @r" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "); + } + + #[test] + fn test_typevar_tuple_binding() { + let mut test = inlay_hint_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.inlay_hints(), @"type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]"); + } + #[test] fn test_many_literals() { let mut test = inlay_hint_test( diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 8e29279fa5..79d111155a 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -219,6 +219,11 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> { self.check_identifier_reference(name); } } + AnyNodeRef::PatternMatchStar(pattern_star) if self.should_include_declaration() => { + if let Some(name) = &pattern_star.name { + self.check_identifier_reference(name); + } + } AnyNodeRef::PatternMatchMapping(pattern_mapping) if self.should_include_declaration() => { @@ -226,6 +231,15 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> { self.check_identifier_reference(rest_name); } } + AnyNodeRef::TypeParamParamSpec(param_spec) if self.should_include_declaration() => { + self.check_identifier_reference(¶m_spec.name); + } + AnyNodeRef::TypeParamTypeVarTuple(param_tuple) if self.should_include_declaration() => { + self.check_identifier_reference(¶m_tuple.name); + } + AnyNodeRef::TypeParamTypeVar(param_var) if self.should_include_declaration() => { + self.check_identifier_reference(¶m_var.name); + } AnyNodeRef::ExprStringLiteral(string_expr) if self.should_include_declaration() => { // Highlight the sub-AST of a string annotation if let Some((sub_ast, sub_model)) = self.model.enter_string_annotation(string_expr) diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 463255f500..156f38fee4 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -495,6 +495,390 @@ class DataProcessor: assert_snapshot!(test.rename("MyNewClass"), @"Cannot rename"); } + #[test] + fn rename_match_name_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | -- + | + "#); + } + + #[test] + fn rename_match_name_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> main.py:4:22 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", ab]: + | ^^ + 5 | x = ab + | -- + | + "#); + } + + #[test] + fn rename_match_rest_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | -- + | + "#); + } + + #[test] + fn rename_match_rest_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", *ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> main.py:4:23 + | + 2 | def my_func(command: str): + 3 | match command.split(): + 4 | case ["get", *ab]: + | ^^ + 5 | x = ab + | -- + | + "#); + } + + #[test] + fn rename_match_as_stmt() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> 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] + fn rename_match_as_binding() { + let test = cursor_test( + r#" + def my_func(command: str): + match command.split(): + case ["get", ("a" | "b") as ab]: + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 2 locations) + --> 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] + fn rename_match_keyword_stmt() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 2 locations) + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | -- + | + "); + } + + #[test] + fn rename_match_keyword_binding() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 2 locations) + --> main.py:10:30 + | + 8 | def my_func(event: Click): + 9 | match event: + 10 | case Click(x, button=ab): + | ^^ + 11 | x = ab + | -- + | + "); + } + + #[test] + fn rename_match_class_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @r#" + info[rename]: Rename symbol (found 3 locations) + --> main.py:2:7 + | + 2 | class Click: + | ^^^^^ + 3 | __match_args__ = ("position", "button") + 4 | def __init__(self, pos, btn): + | + ::: main.py:8:20 + | + 6 | self.button: str = btn + 7 | + 8 | def my_func(event: Click): + | ----- + 9 | match event: + 10 | case Click(x, button=ab): + | ----- + 11 | x = ab + | + "#); + } + + #[test] + fn rename_match_class_field_name() { + let test = cursor_test( + r#" + class Click: + __match_args__ = ("position", "button") + def __init__(self, pos, btn): + self.position: int = pos + self.button: str = btn + + def my_func(event: Click): + match event: + case Click(x, button=ab): + x = ab + "#, + ); + + assert_snapshot!(test.rename("XY"), @"Cannot rename"); + } + + #[test] + fn rename_typevar_name_stmt() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ -- -- + | + "); + } + + #[test] + fn rename_typevar_name_binding() { + let test = cursor_test( + r#" + type Alias1[AB: int = bool] = tuple[AB, list[AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:2:13 + | + 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] + | ^^ -- -- + | + "); + } + + #[test] + fn rename_typevar_spec_stmt() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ -- -- + | + "); + } + + #[test] + fn rename_typevar_spec_binding() { + let test = cursor_test( + r#" + from typing import Callable + type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:3:15 + | + 2 | from typing import Callable + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ -- -- + | + "); + } + + #[test] + fn rename_typevar_tuple_stmt() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ -- -- + | + "); + } + + #[test] + fn rename_typevar_tuple_binding() { + let test = cursor_test( + r#" + type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + "#, + ); + + assert_snapshot!(test.rename("XY"), @r" + info[rename]: Rename symbol (found 3 locations) + --> main.py:2:14 + | + 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] + | ^^ -- -- + | + "); + } + #[test] fn test_cannot_rename_import_module_component() { // Test that we cannot rename parts of module names in import statements diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 5c793d62dc..88e48d1470 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -1060,6 +1060,16 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> { ); } } + ast::Pattern::MatchStar(pattern_star) => { + // Just the one ident here + if let Some(rest_name) = &pattern_star.name { + self.add_token( + rest_name.range(), + SemanticTokenType::Variable, + SemanticTokenModifier::empty(), + ); + } + } _ => { // For all other pattern types, use the default walker ruff_python_ast::visitor::source_order::walk_pattern(self, pattern); @@ -2485,6 +2495,7 @@ def process_data(data): "rest" @ 154..158: Variable "person" @ 181..187: Variable "first" @ 202..207: Variable + "remaining" @ 210..219: Variable "sequence" @ 224..232: Variable "print" @ 246..251: Function "First: " @ 254..261: String diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index 1e5199bc0e..1f7041eaab 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -7,7 +7,7 @@ //! and overloads. use crate::docstring::Docstring; -use crate::goto::DefinitionsOrTargets; +use crate::goto::Definitions; use crate::{Db, find_node::covering_node}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -214,8 +214,7 @@ fn get_callable_documentation( db: &dyn crate::Db, definition: Option, ) -> Option { - DefinitionsOrTargets::Definitions(vec![ResolvedDefinition::Definition(definition?)]) - .docstring(db) + Definitions(vec![ResolvedDefinition::Definition(definition?)]).docstring(db) } /// Create `ParameterDetails` objects from parameter label offsets. diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index d285a035fc..f7b6da1a0f 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -1064,6 +1064,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { .. }) => (name, &None, default), }; + self.scopes_by_expression + .record_expression(name, self.current_scope()); let symbol = self.add_symbol(name.id.clone()); // TODO create Definition for PEP 695 typevars // note that the "bound" on the typevar is a totally different thing than whether