[ty] Improve lsp handling of hover/goto on imports (#21572)

* Fixes https://github.com/astral-sh/ty/issues/1011
* Also fixes the fact that we didn't handle `.x` properly *at all* in
hover/goto

It turns out all of our import handling completely ignored the `level`
(number of relative `.`'s) in a `from ..x.y import z` statement. It was
nice seeing how much my understanding of `ty` has improved -- previously
this would have all been opaque to me but now it was just, completely
glaring and blatant.

Fixing this required refactoring all the import code to take the
importing file into consideration. I ended up refactoring a bunch of
code to pass around/require `SemanticModel` more, as it's the natural
API for resolving this kind of import (it actually had an API for this
that was just... dead code, whoops!).
This commit is contained in:
Aria Desires 2025-11-22 11:06:16 -05:00 committed by GitHub
parent 3410041b4c
commit 859f9ec21a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 403 additions and 89 deletions

View File

@ -61,6 +61,7 @@ pub(crate) enum GotoTarget<'a> {
/// ``` /// ```
ImportModuleComponent { ImportModuleComponent {
module_name: String, module_name: String,
level: u32,
component_index: usize, component_index: usize,
component_range: TextRange, component_range: TextRange,
}, },
@ -302,12 +303,21 @@ impl GotoTarget<'_> {
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`) // (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
GotoTarget::Call { callable, .. } => callable.inferred_type(model), GotoTarget::Call { callable, .. } => callable.inferred_type(model),
GotoTarget::TypeParamTypeVarName(typevar) => typevar.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 // TODO: Support identifier targets
GotoTarget::PatternMatchRest(_) GotoTarget::PatternMatchRest(_)
| GotoTarget::PatternKeywordArgument(_) | GotoTarget::PatternKeywordArgument(_)
| GotoTarget::PatternMatchStarName(_) | GotoTarget::PatternMatchStarName(_)
| GotoTarget::PatternMatchAsName(_) | GotoTarget::PatternMatchAsName(_)
| GotoTarget::ImportModuleComponent { .. }
| GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamParamSpecName(_)
| GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::TypeParamTypeVarTupleName(_)
| GotoTarget::NonLocal { .. } | GotoTarget::NonLocal { .. }
@ -353,37 +363,30 @@ impl GotoTarget<'_> {
/// as just returning a raw `NavigationTarget`. /// as just returning a raw `NavigationTarget`.
pub(crate) fn get_definition_targets<'db>( pub(crate) fn get_definition_targets<'db>(
&self, &self,
file: ruff_db::files::File, model: &SemanticModel<'db>,
db: &'db dyn crate::Db,
alias_resolution: ImportAliasResolution, alias_resolution: ImportAliasResolution,
) -> Option<DefinitionsOrTargets<'db>> { ) -> Option<DefinitionsOrTargets<'db>> {
use crate::NavigationTarget; use crate::NavigationTarget;
let db = model.db();
let file = model.file();
match self { match self {
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression) GotoTarget::Expression(expression) => {
.map(DefinitionsOrTargets::Definitions), definitions_for_expression(model, expression).map(DefinitionsOrTargets::Definitions)
}
// For already-defined symbols, they are their own definitions // For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => { GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![
let model = SemanticModel::new(db, file); ResolvedDefinition::Definition(function.definition(model)),
Some(DefinitionsOrTargets::Definitions(vec![ ])),
ResolvedDefinition::Definition(function.definition(&model)),
]))
}
GotoTarget::ClassDef(class) => { GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![
let model = SemanticModel::new(db, file); ResolvedDefinition::Definition(class.definition(model)),
Some(DefinitionsOrTargets::Definitions(vec![ ])),
ResolvedDefinition::Definition(class.definition(&model)),
]))
}
GotoTarget::Parameter(parameter) => { GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![
let model = SemanticModel::new(db, file); ResolvedDefinition::Definition(parameter.definition(model)),
Some(DefinitionsOrTargets::Definitions(vec![ ])),
ResolvedDefinition::Definition(parameter.definition(&model)),
]))
}
// For import aliases (offset within 'y' or 'z' in "from x import y as z") // For import aliases (offset within 'y' or 'z' in "from x import y as z")
GotoTarget::ImportSymbolAlias { GotoTarget::ImportSymbolAlias {
@ -404,24 +407,18 @@ impl GotoTarget<'_> {
GotoTarget::ImportModuleComponent { GotoTarget::ImportModuleComponent {
module_name, module_name,
component_index, component_index,
level,
.. ..
} => { } => {
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component // We don't currently support hovering the bare `.` so there is always a name
let components: Vec<&str> = module_name.split('.').collect(); let module = import_name(module_name, *component_index);
definitions_for_module(model, Some(module), *level)
// 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)
} }
// Handle import aliases (offset within 'z' in "import x.y as z") // Handle import aliases (offset within 'z' in "import x.y as z")
GotoTarget::ImportModuleAlias { alias } => { GotoTarget::ImportModuleAlias { alias } => {
if alias_resolution == ImportAliasResolution::ResolveAliases { if alias_resolution == ImportAliasResolution::ResolveAliases {
let full_module_name = alias.name.as_str(); definitions_for_module(model, Some(alias.name.as_str()), 0)
// Try to resolve the module
definitions_for_module(db, full_module_name)
} else { } else {
let alias_range = alias.asname.as_ref().unwrap().range; let alias_range = alias.asname.as_ref().unwrap().range;
Some(DefinitionsOrTargets::Targets( Some(DefinitionsOrTargets::Targets(
@ -444,9 +441,8 @@ impl GotoTarget<'_> {
// For exception variables, they are their own definitions (like parameters) // For exception variables, they are their own definitions (like parameters)
GotoTarget::ExceptVariable(except_handler) => { GotoTarget::ExceptVariable(except_handler) => {
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![ 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. // Prefer the function impl over the callable so that its docstrings win if defined.
GotoTarget::Call { callable, call } => { GotoTarget::Call { callable, call } => {
let mut definitions = definitions_for_callable(db, file, call); let mut definitions = definitions_for_callable(model, call);
let expr_definitions = let expr_definitions =
definitions_for_expression(db, file, callable).unwrap_or_default(); definitions_for_expression(model, callable).unwrap_or_default();
definitions.extend(expr_definitions); definitions.extend(expr_definitions);
if definitions.is_empty() { if definitions.is_empty() {
@ -491,18 +487,15 @@ impl GotoTarget<'_> {
} }
GotoTarget::BinOp { expression, .. } => { GotoTarget::BinOp { expression, .. } => {
let model = SemanticModel::new(db, file);
let (definitions, _) = 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)) Some(DefinitionsOrTargets::Definitions(definitions))
} }
GotoTarget::UnaryOp { expression, .. } => { GotoTarget::UnaryOp { expression, .. } => {
let model = SemanticModel::new(db, file);
let (definitions, _) = 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)) Some(DefinitionsOrTargets::Definitions(definitions))
} }
@ -632,6 +625,7 @@ impl GotoTarget<'_> {
{ {
return Some(GotoTarget::ImportModuleComponent { return Some(GotoTarget::ImportModuleComponent {
module_name: full_name.to_string(), module_name: full_name.to_string(),
level: 0,
component_index, component_index,
component_range, component_range,
}); });
@ -672,14 +666,12 @@ impl GotoTarget<'_> {
// Handle offset within module name in from import statements // Handle offset within module name in from import statements
if let Some(module_expr) = &from.module { if let Some(module_expr) = &from.module {
let full_module_name = module_expr.to_string(); let full_module_name = module_expr.to_string();
if let Some((component_index, component_range)) =
if let Some((component_index, component_range)) = find_module_component( find_module_component(&full_module_name, module_expr.start(), offset)
&full_module_name, {
module_expr.range.start(),
offset,
) {
return Some(GotoTarget::ImportModuleComponent { return Some(GotoTarget::ImportModuleComponent {
module_name: full_module_name, module_name: full_module_name,
level: from.level,
component_index, component_index,
component_range, 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) /// Shared helper to get definitions for an expr (that is presumably a name/attr)
fn definitions_for_expression<'db>( fn definitions_for_expression<'db>(
db: &'db dyn crate::Db, model: &SemanticModel<'db>,
file: ruff_db::files::File,
expression: &ruff_python_ast::ExprRef<'_>, expression: &ruff_python_ast::ExprRef<'_>,
) -> Option<Vec<ResolvedDefinition<'db>>> { ) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression { 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( ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
db, file, attribute, model.db(),
model.file(),
attribute,
)), )),
_ => None, _ => None,
} }
} }
fn definitions_for_callable<'db>( fn definitions_for_callable<'db>(
db: &'db dyn crate::Db, model: &SemanticModel<'db>,
file: ruff_db::files::File,
call: &ast::ExprCall, call: &ast::ExprCall,
) -> Vec<ResolvedDefinition<'db>> { ) -> Vec<ResolvedDefinition<'db>> {
let model = SemanticModel::new(db, file);
// Attempt to refine to a specific call // 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 signature_info
.into_iter() .into_iter()
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition)) .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()) 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()?; .ok()?;
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens()) 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. /// Helper function to resolve a module name and create a navigation target.
fn definitions_for_module<'db>( fn definitions_for_module<'db>(
db: &'db dyn crate::Db, model: &SemanticModel,
module_name_str: &str, module: Option<&str>,
level: u32,
) -> Option<DefinitionsOrTargets<'db>> { ) -> Option<DefinitionsOrTargets<'db>> {
use ty_python_semantic::{ModuleName, resolve_module}; let module = model.resolve_module(module, level)?;
let file = module.file(model.db())?;
if let Some(module_name) = ModuleName::new(module_name_str) { Some(DefinitionsOrTargets::Definitions(vec![
if let Some(resolved_module) = resolve_module(db, &module_name) { ResolvedDefinition::Module(file),
if let Some(module_file) = resolved_module.file(db) { ]))
return Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Module(module_file),
]));
}
}
}
None
} }
/// Helper function to extract module component information from a dotted module name /// 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 // Split the module name into components and find which one contains the offset
let mut current_pos = 0; let mut current_pos = 0;
let components: Vec<&str> = full_module_name.split('.').collect(); for (i, component) in full_module_name.split('.').enumerate() {
for (i, component) in components.iter().enumerate() {
let component_start = current_pos; let component_start = current_pos;
let component_end = current_pos + component.len(); let component_end = current_pos + component.len();
@ -1004,3 +989,16 @@ fn find_module_component(
None 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]
}

View File

@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange}; use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution; use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the declaration of a symbol. /// Navigate to the declaration of a symbol.
/// ///
@ -18,8 +18,9 @@ pub fn goto_declaration(
let module = parsed_module(db, file).load(db); let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?; let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let declaration_targets = goto_target let declaration_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)? .get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.declaration_targets(db)?; .declaration_targets(db)?;
Some(RangedValue { Some(RangedValue {

View File

@ -3,7 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange}; use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution; use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Navigate to the definition of a symbol. /// Navigate to the definition of a symbol.
/// ///
@ -18,9 +18,9 @@ pub fn goto_definition(
) -> Option<RangedValue<NavigationTargets>> { ) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db); let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?; let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db, file);
let definition_targets = goto_target let definition_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)? .get_definition_targets(&model, ImportAliasResolution::ResolveAliases)?
.definition_targets(db)?; .definition_targets(db)?;
Some(RangedValue { Some(RangedValue {

View File

@ -285,6 +285,300 @@ mod tests {
"); ");
} }
#[test]
fn goto_type_of_import_module() {
let mut test = cursor_test(
r#"
import l<CURSOR>ib
"#,
);
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 li<CURSOR>b.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.subm<CURSOR>od
"#,
);
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 l<CURSOR>ib 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 li<CURSOR>b.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.subm<CURSOR>od 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.bot<CURSOR>mod 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 .bo<CURSOR>t.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 .<CURSOR>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 .<CURSOR> 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] #[test]
fn goto_type_of_expression_with_module() { fn goto_type_of_expression_with_module() {
let mut test = cursor_test( let mut test = cursor_test(

View File

@ -22,8 +22,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
let model = SemanticModel::new(db, file); let model = SemanticModel::new(db, file);
let docs = goto_target let docs = goto_target
.get_definition_targets( .get_definition_targets(
file, &model,
db,
ty_python_semantic::ImportAliasResolution::ResolveAliases, ty_python_semantic::ImportAliasResolution::ResolveAliases,
) )
.and_then(|definitions| definitions.docstring(db)) .and_then(|definitions| definitions.docstring(db))
@ -1488,11 +1487,17 @@ def ab(a: int, *, c: int):
.unwrap(); .unwrap();
assert_snapshot!(test.hover(), @r" assert_snapshot!(test.hover(), @r"
<module 'lib'>
---------------------------------------------
The cool lib_py module! The cool lib_py module!
Wow this module rocks. Wow this module rocks.
--------------------------------------------- ---------------------------------------------
```python
<module 'lib'>
```
---
The cool lib/_py module! The cool lib/_py module!
Wow this module rocks. Wow this module rocks.

View File

@ -20,7 +20,7 @@ use ruff_python_ast::{
}; };
use ruff_python_parser::Tokens; use ruff_python_parser::Tokens;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use ty_python_semantic::ImportAliasResolution; use ty_python_semantic::{ImportAliasResolution, SemanticModel};
/// Mode for references search behavior /// Mode for references search behavior
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -48,8 +48,9 @@ pub(crate) fn references(
// Get the definitions for the symbol at the cursor position // Get the definitions for the symbol at the cursor position
// When finding references, do not resolve any local aliases. // When finding references, do not resolve any local aliases.
let model = SemanticModel::new(db, file);
let target_definitions_nav = goto_target let target_definitions_nav = goto_target
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases)? .get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
.definition_targets(db)?; .definition_targets(db)?;
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect(); let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
@ -289,8 +290,9 @@ impl LocalReferencesFinder<'_> {
GotoTarget::from_covering_node(covering_node, offset, self.tokens) GotoTarget::from_covering_node(covering_node, offset, self.tokens)
{ {
// Get the definitions for this goto target // Get the definitions for this goto target
let model = SemanticModel::new(self.db, self.file);
if let Some(current_definitions_nav) = goto_target 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)) .and_then(|definitions| definitions.declaration_targets(self.db))
{ {
let current_definitions: Vec<NavigationTarget> = let current_definitions: Vec<NavigationTarget> =

View File

@ -3,12 +3,13 @@ use crate::references::{ReferencesMode, references};
use crate::{Db, ReferenceTarget}; use crate::{Db, ReferenceTarget};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_text_size::{Ranged, TextSize}; 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. /// 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<ruff_text_size::TextRange> { pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option<ruff_text_size::TextRange> {
let parsed = ruff_db::parsed::parsed_module(db, file); let parsed = ruff_db::parsed::parsed_module(db, file);
let module = parsed.load(db); let module = parsed.load(db);
let model = SemanticModel::new(db, file);
// Get the definitions for the symbol at the offset // Get the definitions for the symbol at the offset
let goto_target = find_goto_target(&module, 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<ruff_text
let current_file_in_project = is_file_in_project(db, file); let current_file_in_project = is_file_in_project(db, file);
if let Some(definition_targets) = goto_target if let Some(definition_targets) = goto_target
.get_definition_targets(file, db, ImportAliasResolution::PreserveAliases) .get_definition_targets(&model, ImportAliasResolution::PreserveAliases)
.and_then(|definitions| definitions.declaration_targets(db)) .and_then(|definitions| definitions.declaration_targets(db))
{ {
for target in &definition_targets { for target in &definition_targets {

View File

@ -31,6 +31,10 @@ impl<'db> SemanticModel<'db> {
self.db self.db
} }
pub fn file(&self) -> File {
self.file
}
pub fn file_path(&self) -> &FilePath { pub fn file_path(&self) -> &FilePath {
self.file.path(self.db) self.file.path(self.db)
} }
@ -70,8 +74,17 @@ impl<'db> SemanticModel<'db> {
members members
} }
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module<'_>> { /// Resolve the given import made in this file to a Type
resolve_module(self.db, module_name) pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option<Type<'db>> {
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<Module<'db>> {
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 <CURSOR>` context. /// Returns completions for symbols available in a `import <CURSOR>` context.