diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 2e390711b7..2598e5efe1 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -386,6 +386,29 @@ FOO = 0 "); } + #[test] + fn goto_declaration_from_import_rhs_is_module() { + let test = CursorTest::builder() + .source("lib/__init__.py", r#""#) + .source("lib/module.py", r#""#) + .source("main.py", r#"from lib import module"#) + .build(); + + // Should resolve to the actual function definition, not the import statement + assert_snapshot!(test.goto_declaration(), @r" + info[goto-declaration]: Go to declaration + --> main.py:1:17 + | + 1 | from lib import module + | ^^^^^^ Clicking here + | + info: Found 1 declaration + --> lib/module.py:1:1 + | + | + "); + } + #[test] fn goto_declaration_from_import_as() { let test = CursorTest::builder() diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 8de1d00d28..b67306dc1d 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1041,21 +1041,26 @@ mod resolve_definition { } } - // Resolve the target module file - let module_file = { - // Resolve the module being imported from (handles both relative and absolute imports) - let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok() - else { - return Vec::new(); - }; - let Some(resolved_module) = resolve_module(db, file, &module_name) else { - return Vec::new(); - }; - resolved_module.file(db) + // Resolve the module being imported from (handles both relative and absolute imports) + let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok() + else { + return Vec::new(); + }; + let Some(resolved_module) = resolve_module(db, file, &module_name) else { + return Vec::new(); }; + // Resolve the target module file + let module_file = resolved_module.file(db); + let Some(module_file) = module_file else { - return Vec::new(); // Module resolution failed + // No file means this is a namespace package, try to import the submodule + return Vec::from_iter(resolve_from_import_submodule_definitions( + db, + file, + symbol_name, + module_name, + )); }; // Find the definition of this symbol in the imported module's global scope @@ -1064,22 +1069,38 @@ mod resolve_definition { // Recursively resolve any import definitions found in the target module if definitions_in_module.is_empty() { - // If we can't find the specific symbol, return empty list - Vec::new() - } else { - let mut resolved_definitions = Vec::new(); - for def in definitions_in_module { - let resolved = resolve_definition_recursive( - db, - def, - visited, - Some(symbol_name), - alias_resolution, - ); - resolved_definitions.extend(resolved); - } - resolved_definitions + // This might be importing a submodule, try that + return Vec::from_iter(resolve_from_import_submodule_definitions( + db, + file, + symbol_name, + module_name, + )); } + + let mut resolved_definitions = Vec::new(); + for def in definitions_in_module { + let resolved = + resolve_definition_recursive(db, def, visited, Some(symbol_name), alias_resolution); + resolved_definitions.extend(resolved); + } + resolved_definitions + } + + // Helper to resolve `from x.y import z` assuming `x.y.z` is a module. + fn resolve_from_import_submodule_definitions<'db>( + db: &'db dyn Db, + file: File, + symbol_name: &str, + module_name: ModuleName, + ) -> Option> { + let submodule_name = ModuleName::new(symbol_name)?; + let mut full_submodule_name = module_name; + full_submodule_name.extend(&submodule_name); + let module = resolve_module(db, file, &full_submodule_name)?; + let file = module.file(db)?; + + Some(ResolvedDefinition::Module(file)) } /// Find definitions for a symbol name in a specific scope.