diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 6a0614d3941f5..848ca0e2e584e 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> { /// ``` ImportModuleComponent { module_name: String, + level: u32, component_index: usize, component_range: TextRange, }, @@ -302,12 +303,21 @@ impl GotoTarget<'_> { // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) GotoTarget::Call { callable, .. } => callable.inferred_type(model), GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model), + GotoTarget::ImportModuleComponent { + module_name, + component_index, + level, + .. + } => { + // We don't currently support hovering the bare `.` so there is always a name + let module = import_name(module_name, *component_index); + model.resolve_module_type(Some(module), *level)? + } // TODO: Support identifier targets GotoTarget::PatternMatchRest(_) | GotoTarget::PatternKeywordArgument(_) | GotoTarget::PatternMatchStarName(_) | GotoTarget::PatternMatchAsName(_) - | GotoTarget::ImportModuleComponent { .. } | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } @@ -353,37 +363,30 @@ impl GotoTarget<'_> { /// as just returning a raw `NavigationTarget`. pub(crate) fn get_definition_targets<'db>( &self, - file: ruff_db::files::File, - db: &'db dyn crate::Db, + model: &SemanticModel<'db>, alias_resolution: ImportAliasResolution, ) -> Option> { use crate::NavigationTarget; + let db = model.db(); + let file = model.file(); match self { - GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression) - .map(DefinitionsOrTargets::Definitions), + GotoTarget::Expression(expression) => { + definitions_for_expression(model, expression).map(DefinitionsOrTargets::Definitions) + } // For already-defined symbols, they are their own definitions - GotoTarget::FunctionDef(function) => { - let model = SemanticModel::new(db, file); - Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(function.definition(&model)), - ])) - } + GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![ + ResolvedDefinition::Definition(function.definition(model)), + ])), - GotoTarget::ClassDef(class) => { - let model = SemanticModel::new(db, file); - Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(class.definition(&model)), - ])) - } + GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![ + ResolvedDefinition::Definition(class.definition(model)), + ])), - GotoTarget::Parameter(parameter) => { - let model = SemanticModel::new(db, file); - Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(parameter.definition(&model)), - ])) - } + GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![ + ResolvedDefinition::Definition(parameter.definition(model)), + ])), // For import aliases (offset within 'y' or 'z' in "from x import y as z") GotoTarget::ImportSymbolAlias { @@ -404,24 +407,18 @@ impl GotoTarget<'_> { GotoTarget::ImportModuleComponent { module_name, component_index, + level, .. } => { - // Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component - let components: Vec<&str> = module_name.split('.').collect(); - - // Build the module name up to and including the component containing the offset - let target_module_name = components[..=*component_index].join("."); - - // Try to resolve the module - definitions_for_module(db, &target_module_name) + // We don't currently support hovering the bare `.` so there is always a name + let module = import_name(module_name, *component_index); + definitions_for_module(model, Some(module), *level) } // Handle import aliases (offset within 'z' in "import x.y as z") GotoTarget::ImportModuleAlias { alias } => { if alias_resolution == ImportAliasResolution::ResolveAliases { - let full_module_name = alias.name.as_str(); - // Try to resolve the module - definitions_for_module(db, full_module_name) + definitions_for_module(model, Some(alias.name.as_str()), 0) } else { let alias_range = alias.asname.as_ref().unwrap().range; Some(DefinitionsOrTargets::Targets( @@ -444,9 +441,8 @@ impl GotoTarget<'_> { // For exception variables, they are their own definitions (like parameters) GotoTarget::ExceptVariable(except_handler) => { - let model = SemanticModel::new(db, file); Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Definition(except_handler.definition(&model)), + ResolvedDefinition::Definition(except_handler.definition(model)), ])) } @@ -478,9 +474,9 @@ impl GotoTarget<'_> { // // Prefer the function impl over the callable so that its docstrings win if defined. GotoTarget::Call { callable, call } => { - let mut definitions = definitions_for_callable(db, file, call); + let mut definitions = definitions_for_callable(model, call); let expr_definitions = - definitions_for_expression(db, file, callable).unwrap_or_default(); + definitions_for_expression(model, callable).unwrap_or_default(); definitions.extend(expr_definitions); if definitions.is_empty() { @@ -491,18 +487,15 @@ impl GotoTarget<'_> { } GotoTarget::BinOp { expression, .. } => { - let model = SemanticModel::new(db, file); - let (definitions, _) = - ty_python_semantic::definitions_for_bin_op(db, &model, expression)?; + ty_python_semantic::definitions_for_bin_op(db, model, expression)?; Some(DefinitionsOrTargets::Definitions(definitions)) } GotoTarget::UnaryOp { expression, .. } => { - let model = SemanticModel::new(db, file); let (definitions, _) = - ty_python_semantic::definitions_for_unary_op(db, &model, expression)?; + ty_python_semantic::definitions_for_unary_op(db, model, expression)?; Some(DefinitionsOrTargets::Definitions(definitions)) } @@ -632,6 +625,7 @@ impl GotoTarget<'_> { { return Some(GotoTarget::ImportModuleComponent { module_name: full_name.to_string(), + level: 0, component_index, component_range, }); @@ -672,14 +666,12 @@ impl GotoTarget<'_> { // Handle offset within module name in from import statements if let Some(module_expr) = &from.module { let full_module_name = module_expr.to_string(); - - if let Some((component_index, component_range)) = find_module_component( - &full_module_name, - module_expr.range.start(), - offset, - ) { + if let Some((component_index, component_range)) = + find_module_component(&full_module_name, module_expr.start(), offset) + { return Some(GotoTarget::ImportModuleComponent { module_name: full_module_name, + level: from.level, component_index, component_range, }); @@ -876,27 +868,26 @@ fn convert_resolved_definitions_to_targets( /// Shared helper to get definitions for an expr (that is presumably a name/attr) fn definitions_for_expression<'db>( - db: &'db dyn crate::Db, - file: ruff_db::files::File, + model: &SemanticModel<'db>, expression: &ruff_python_ast::ExprRef<'_>, ) -> Option>> { match expression { - ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)), + ast::ExprRef::Name(name) => Some(definitions_for_name(model.db(), model.file(), name)), ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute( - db, file, attribute, + model.db(), + model.file(), + attribute, )), _ => None, } } fn definitions_for_callable<'db>( - db: &'db dyn crate::Db, - file: ruff_db::files::File, + model: &SemanticModel<'db>, call: &ast::ExprCall, ) -> Vec> { - let model = SemanticModel::new(db, file); // Attempt to refine to a specific call - let signature_info = call_signature_details(db, &model, call); + let signature_info = call_signature_details(model.db(), model, call); signature_info .into_iter() .filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition)) @@ -947,7 +938,9 @@ pub(crate) fn find_goto_target( } let covering_node = covering_node(parsed.syntax().into(), token.range()) - .find_first(|node| node.is_identifier() || node.is_expression()) + .find_first(|node| { + node.is_identifier() || node.is_expression() || node.is_stmt_import_from() + }) .ok()?; GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens()) @@ -955,21 +948,15 @@ pub(crate) fn find_goto_target( /// Helper function to resolve a module name and create a navigation target. fn definitions_for_module<'db>( - db: &'db dyn crate::Db, - module_name_str: &str, + model: &SemanticModel, + module: Option<&str>, + level: u32, ) -> Option> { - use ty_python_semantic::{ModuleName, resolve_module}; - - if let Some(module_name) = ModuleName::new(module_name_str) { - if let Some(resolved_module) = resolve_module(db, &module_name) { - if let Some(module_file) = resolved_module.file(db) { - return Some(DefinitionsOrTargets::Definitions(vec![ - ResolvedDefinition::Module(module_file), - ])); - } - } - } - None + let module = model.resolve_module(module, level)?; + let file = module.file(model.db())?; + Some(DefinitionsOrTargets::Definitions(vec![ + ResolvedDefinition::Module(file), + ])) } /// Helper function to extract module component information from a dotted module name @@ -983,9 +970,7 @@ fn find_module_component( // Split the module name into components and find which one contains the offset let mut current_pos = 0; - let components: Vec<&str> = full_module_name.split('.').collect(); - - for (i, component) in components.iter().enumerate() { + for (i, component) in full_module_name.split('.').enumerate() { let component_start = current_pos; let component_end = current_pos + component.len(); @@ -1004,3 +989,16 @@ fn find_module_component( None } + +/// Helper to get the module name up to the given component index +fn import_name(module_name: &str, component_index: usize) -> &str { + // We want everything to the left of the nth `.` + // If there's no nth `.` then we want the whole thing. + let idx = module_name + .match_indices('.') + .nth(component_index) + .map(|(idx, _)| idx) + .unwrap_or(module_name.len()); + + &module_name[..idx] +} diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index bd67246b0375f..6b02af9adf9d4 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; -use ty_python_semantic::ImportAliasResolution; +use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the declaration of a symbol. /// @@ -18,8 +18,9 @@ pub fn goto_declaration( let module = parsed_module(db, file).load(db); let goto_target = find_goto_target(&module, offset)?; + let model = SemanticModel::new(db, file); let declaration_targets = goto_target - .get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)? + .get_definition_targets(&model, ImportAliasResolution::ResolveAliases)? .declaration_targets(db)?; Some(RangedValue { diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 02d2d1b9a6ce2..f46a03b1ccbfc 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; -use ty_python_semantic::ImportAliasResolution; +use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the definition of a symbol. /// @@ -18,9 +18,9 @@ pub fn goto_definition( ) -> Option> { let module = parsed_module(db, file).load(db); let goto_target = find_goto_target(&module, offset)?; - + let model = SemanticModel::new(db, file); let definition_targets = goto_target - .get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)? + .get_definition_targets(&model, ImportAliasResolution::ResolveAliases)? .definition_targets(db)?; Some(RangedValue { diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index ffdad707a0c19..0b3e0c9bca6d7 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -285,6 +285,300 @@ mod tests { "); } + #[test] + fn goto_type_of_import_module() { + let mut test = cursor_test( + r#" + import lib + "#, + ); + + test.write_file("lib.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ^^^^^^ + | + info: Source + --> main.py:2:8 + | + 2 | import lib + | ^^^ + | + "); + } + + #[test] + fn goto_type_of_import_module_multi1() { + let mut test = cursor_test( + r#" + import lib.submod + "#, + ); + + test.write_file("lib/__init__.py", "b = 7").unwrap(); + test.write_file("lib/submod.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ^^^^^ + | + info: Source + --> main.py:2:8 + | + 2 | import lib.submod + | ^^^ + | + "); + } + + #[test] + fn goto_type_of_import_module_multi2() { + let mut test = cursor_test( + r#" + import lib.submod + "#, + ); + + test.write_file("lib/__init__.py", "b = 7").unwrap(); + test.write_file("lib/submod.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ^^^^^^ + | + info: Source + --> main.py:2:12 + | + 2 | import lib.submod + | ^^^^^^ + | + "); + } + + #[test] + fn goto_type_of_from_import_module() { + let mut test = cursor_test( + r#" + from lib import a + "#, + ); + + test.write_file("lib.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib.py:1:1 + | + 1 | a = 10 + | ^^^^^^ + | + info: Source + --> main.py:2:6 + | + 2 | from lib import a + | ^^^ + | + "); + } + + #[test] + fn goto_type_of_from_import_module_multi1() { + let mut test = cursor_test( + r#" + from lib.submod import a + "#, + ); + + test.write_file("lib/__init__.py", "b = 7").unwrap(); + test.write_file("lib/submod.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/__init__.py:1:1 + | + 1 | b = 7 + | ^^^^^ + | + info: Source + --> main.py:2:6 + | + 2 | from lib.submod import a + | ^^^ + | + "); + } + + #[test] + fn goto_type_of_from_import_module_multi2() { + let mut test = cursor_test( + r#" + from lib.submod import a + "#, + ); + + test.write_file("lib/__init__.py", "b = 7").unwrap(); + test.write_file("lib/submod.py", "a = 10").unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/submod.py:1:1 + | + 1 | a = 10 + | ^^^^^^ + | + info: Source + --> main.py:2:10 + | + 2 | from lib.submod import a + | ^^^^^^ + | + "); + } + + #[test] + fn goto_type_of_from_import_rel1() { + let mut test = CursorTest::builder() + .source( + "lib/sub/__init__.py", + r#" + from .bot.botmod import * + sub = 2 + "#, + ) + .build(); + + test.write_file("lib/__init__.py", "lib = 1").unwrap(); + // test.write_file("lib/sub/__init__.py", "sub = 2").unwrap(); + test.write_file("lib/sub/bot/__init__.py", "bot = 3") + .unwrap(); + test.write_file("lib/sub/submod.py", "submod = 21").unwrap(); + test.write_file("lib/sub/bot/botmod.py", "botmod = 31") + .unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/sub/bot/botmod.py:1:1 + | + 1 | botmod = 31 + | ^^^^^^^^^^^ + | + info: Source + --> lib/sub/__init__.py:2:11 + | + 2 | from .bot.botmod import * + | ^^^^^^ + 3 | sub = 2 + | + "); + } + + #[test] + fn goto_type_of_from_import_rel2() { + let mut test = CursorTest::builder() + .source( + "lib/sub/__init__.py", + r#" + from .bot.botmod import * + sub = 2 + "#, + ) + .build(); + + test.write_file("lib/__init__.py", "lib = 1").unwrap(); + // test.write_file("lib/sub/__init__.py", "sub = 2").unwrap(); + test.write_file("lib/sub/bot/__init__.py", "bot = 3") + .unwrap(); + test.write_file("lib/sub/submod.py", "submod = 21").unwrap(); + test.write_file("lib/sub/bot/botmod.py", "botmod = 31") + .unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ^^^^^^^ + | + info: Source + --> lib/sub/__init__.py:2:7 + | + 2 | from .bot.botmod import * + | ^^^ + 3 | sub = 2 + | + "); + } + + #[test] + fn goto_type_of_from_import_rel3() { + let mut test = CursorTest::builder() + .source( + "lib/sub/__init__.py", + r#" + from .bot.botmod import * + sub = 2 + "#, + ) + .build(); + + test.write_file("lib/__init__.py", "lib = 1").unwrap(); + // test.write_file("lib/sub/__init__.py", "sub = 2").unwrap(); + test.write_file("lib/sub/bot/__init__.py", "bot = 3") + .unwrap(); + test.write_file("lib/sub/submod.py", "submod = 21").unwrap(); + test.write_file("lib/sub/bot/botmod.py", "botmod = 31") + .unwrap(); + + assert_snapshot!(test.goto_type_definition(), @r" + info[goto-type-definition]: Type definition + --> lib/sub/bot/__init__.py:1:1 + | + 1 | bot = 3 + | ^^^^^^^ + | + info: Source + --> lib/sub/__init__.py:2:7 + | + 2 | from .bot.botmod import * + | ^^^ + 3 | sub = 2 + | + "); + } + + #[test] + fn goto_type_of_from_import_rel4() { + let mut test = CursorTest::builder() + .source( + "lib/sub/__init__.py", + r#" + from . import submod + sub = 2 + "#, + ) + .build(); + + test.write_file("lib/__init__.py", "lib = 1").unwrap(); + // test.write_file("lib/sub/__init__.py", "sub = 2").unwrap(); + test.write_file("lib/sub/bot/__init__.py", "bot = 3") + .unwrap(); + test.write_file("lib/sub/submod.py", "submod = 21").unwrap(); + test.write_file("lib/sub/bot/botmod.py", "botmod = 31") + .unwrap(); + + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + #[test] fn goto_type_of_expression_with_module() { let mut test = cursor_test( diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 1b8716f18efbb..c8dbf0b5e5d27 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -22,8 +22,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option + --------------------------------------------- The cool lib_py module! Wow this module rocks. --------------------------------------------- + ```python + + ``` + --- The cool lib/_py module! Wow this module rocks. diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 62716fd6a5404..c7cdf52386b9a 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -20,7 +20,7 @@ use ruff_python_ast::{ }; use ruff_python_parser::Tokens; use ruff_text_size::{Ranged, TextRange}; -use ty_python_semantic::ImportAliasResolution; +use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Mode for references search behavior #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -48,8 +48,9 @@ pub(crate) fn references( // Get the definitions for the symbol at the cursor position // When finding references, do not resolve any local aliases. + let model = SemanticModel::new(db, file); let target_definitions_nav = goto_target - .get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)? + .get_definition_targets(&model, ImportAliasResolution::PreserveAliases)? .definition_targets(db)?; let target_definitions: Vec = target_definitions_nav.into_iter().collect(); @@ -289,8 +290,9 @@ impl LocalReferencesFinder<'_> { GotoTarget::from_covering_node(covering_node, offset, self.tokens) { // Get the definitions for this goto target + let model = SemanticModel::new(self.db, self.file); if let Some(current_definitions_nav) = goto_target - .get_definition_targets(self.file, self.db, ImportAliasResolution::PreserveAliases) + .get_definition_targets(&model, ImportAliasResolution::PreserveAliases) .and_then(|definitions| definitions.declaration_targets(self.db)) { let current_definitions: Vec = diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 12be3b9cad65a..d8773a30d06ef 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -3,12 +3,13 @@ use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; use ruff_db::files::File; use ruff_text_size::{Ranged, TextSize}; -use ty_python_semantic::ImportAliasResolution; +use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Returns the range of the symbol if it can be renamed, None if not. pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option { let parsed = ruff_db::parsed::parsed_module(db, file); let module = parsed.load(db); + let model = SemanticModel::new(db, file); // Get the definitions for the symbol at the offset let goto_target = find_goto_target(&module, offset)?; @@ -24,7 +25,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option SemanticModel<'db> { self.db } + pub fn file(&self) -> File { + self.file + } + pub fn file_path(&self) -> &FilePath { self.file.path(self.db) } @@ -70,8 +74,17 @@ impl<'db> SemanticModel<'db> { members } - pub fn resolve_module(&self, module_name: &ModuleName) -> Option> { - resolve_module(self.db, module_name) + /// Resolve the given import made in this file to a Type + pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option> { + let module = self.resolve_module(module, level)?; + Some(Type::module_literal(self.db, self.file, module)) + } + + /// Resolve the given import made in this file to a Module + pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option> { + let module_name = + ModuleName::from_identifier_parts(self.db, self.file, module, level).ok()?; + resolve_module(self.db, &module_name) } /// Returns completions for symbols available in a `import ` context.