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