Skip to content

Commit f0b03c3

Browse files
authored
[ty] resolve docstrings for modules (#19898)
This also reintroduces the `ResolvedDefinition::Module` variant because reverse-engineering it in several places is a bit confusing. In an ideal world we wouldn't have `ResolvedDefinition::FileWithRange` as it kinda kills the ability to do richer analysis, so I want to chip away at its scope wherever I can (currently it's used to point at asname parts of import statements when doing `ImportAliasResolution::PreserveAliases`, and also keyword arguments). This also makes a kind of odd change to allow a hover to *only* produce a docstring. This works around an oddity where hovering over a module name in an import fails to resolve to a `ty` even though hovering over uses of that imported name *does*. The two fixed tests reflect the two interesting cases here.
1 parent 9f6146a commit f0b03c3

File tree

4 files changed

+93
-49
lines changed

4 files changed

+93
-49
lines changed

crates/ty_ide/src/goto.rs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::borrow::Cow;
77

88
use crate::find_node::covering_node;
99
use crate::stub_mapping::StubMapper;
10-
use ruff_db::files::FileRange;
1110
use ruff_db::parsed::ParsedModuleRef;
1211
use ruff_python_ast::{self as ast, AnyNodeRef};
1312
use ruff_python_parser::TokenKind;
@@ -203,15 +202,8 @@ impl<'db> DefinitionsOrTargets<'db> {
203202
DefinitionsOrTargets::Targets(_) => return None,
204203
};
205204
for definition in &definitions {
206-
// TODO: get docstrings for modules
207-
let ResolvedDefinition::Definition(definition) = definition else {
208-
continue;
209-
};
210-
// First try to get the docstring from the original definition
211-
let original_docstring = definition.docstring(db);
212-
213205
// If we got a docstring from the original definition, use it
214-
if let Some(docstring) = original_docstring {
206+
if let Some(docstring) = definition.docstring(db) {
215207
return Some(Docstring::new(docstring));
216208
}
217209
}
@@ -222,12 +214,9 @@ impl<'db> DefinitionsOrTargets<'db> {
222214
let stub_mapper = StubMapper::new(db);
223215

224216
// Try to find the corresponding implementation definition
225-
for mapped_definition in stub_mapper.map_definitions(definitions) {
226-
// TODO: get docstrings for modules
227-
if let ResolvedDefinition::Definition(impl_definition) = mapped_definition {
228-
if let Some(impl_docstring) = impl_definition.docstring(db) {
229-
return Some(Docstring::new(impl_docstring));
230-
}
217+
for definition in stub_mapper.map_definitions(definitions) {
218+
if let Some(docstring) = definition.docstring(db) {
219+
return Some(Docstring::new(docstring));
231220
}
232221
}
233222

@@ -703,6 +692,10 @@ fn convert_resolved_definitions_to_targets(
703692
full_range: full_range.range(),
704693
}
705694
}
695+
ty_python_semantic::ResolvedDefinition::Module(file) => {
696+
// For modules, navigate to the start of the file
697+
crate::NavigationTarget::new(file, TextRange::default())
698+
}
706699
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
707700
// For file ranges, navigate to the specific range within the file
708701
crate::NavigationTarget::new(file_range.file(), file_range.range())
@@ -761,11 +754,9 @@ fn definitions_for_module<'db>(
761754
if let Some(module_name) = ModuleName::new(module_name_str) {
762755
if let Some(resolved_module) = resolve_module(db, &module_name) {
763756
if let Some(module_file) = resolved_module.file(db) {
764-
let definitions = vec![ResolvedDefinition::FileWithRange(FileRange::new(
765-
module_file,
766-
TextRange::default(),
767-
))];
768-
return Some(DefinitionsOrTargets::Definitions(definitions));
757+
return Some(DefinitionsOrTargets::Definitions(vec![
758+
ResolvedDefinition::Module(module_file),
759+
]));
769760
}
770761
}
771762
}

crates/ty_ide/src/hover.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
2020
}
2121

2222
let model = SemanticModel::new(db, file);
23-
let ty = goto_target.inferred_type(&model)?;
23+
let ty = goto_target.inferred_type(&model).map(HoverContent::Type);
2424
let docs = goto_target
2525
.get_definition_targets(
2626
file,
@@ -29,12 +29,20 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
2929
)
3030
.and_then(|definitions| definitions.docstring(db))
3131
.map(HoverContent::Docstring);
32-
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
32+
33+
if let Some(HoverContent::Type(ty)) = ty {
34+
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
35+
}
3336

3437
// TODO: Render the symbol's signature instead of just its type.
35-
let mut contents = vec![HoverContent::Type(ty)];
38+
let mut contents = Vec::new();
39+
contents.extend(ty);
3640
contents.extend(docs);
3741

42+
if contents.is_empty() {
43+
return None;
44+
}
45+
3846
Some(RangedValue {
3947
range: FileRange::new(file, goto_target.range()),
4048
value: Hover { contents },
@@ -630,9 +638,21 @@ mod tests {
630638

631639
assert_snapshot!(test.hover(), @r"
632640
<module 'lib'>
641+
---------------------------------------------
642+
The cool lib_py module!
643+
644+
Wow this module rocks.
645+
633646
---------------------------------------------
634647
```python
635648
<module 'lib'>
649+
```
650+
---
651+
```text
652+
The cool lib_py module!
653+
654+
Wow this module rocks.
655+
636656
```
637657
---------------------------------------------
638658
info[hover]: Hovered content is
@@ -672,7 +692,31 @@ mod tests {
672692
)
673693
.unwrap();
674694

675-
assert_snapshot!(test.hover(), @"Hover provided no content");
695+
assert_snapshot!(test.hover(), @r"
696+
The cool lib_py module!
697+
698+
Wow this module rocks.
699+
700+
---------------------------------------------
701+
```text
702+
The cool lib_py module!
703+
704+
Wow this module rocks.
705+
706+
```
707+
---------------------------------------------
708+
info[hover]: Hovered content is
709+
--> main.py:2:20
710+
|
711+
2 | import lib
712+
| ^^-
713+
| | |
714+
| | Cursor offset
715+
| source
716+
3 |
717+
4 | lib
718+
|
719+
");
676720
}
677721

678722
#[test]

crates/ty_python_semantic/src/semantic_index/definition.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ impl<'db> Definition<'db> {
113113
}
114114
}
115115

116-
/// Extract a docstring from a function or class body.
116+
/// Get the module-level docstring for the given file
117+
pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option<String> {
118+
let module = parsed_module(db, file).load(db);
119+
docstring_from_body(module.suite())
120+
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
121+
}
122+
123+
/// Extract a docstring from a function, module, or class body.
117124
fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
118125
let stmt = body.first()?;
119126
// Require the docstring to be a standalone expression.

crates/ty_python_semantic/src/types/ide_support.rs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -973,12 +973,11 @@ mod resolve_definition {
973973
use ruff_db::files::{File, FileRange};
974974
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
975975
use ruff_python_ast as ast;
976-
use ruff_text_size::{Ranged, TextRange};
977976
use rustc_hash::FxHashSet;
978977
use tracing::trace;
979978

980979
use crate::module_resolver::file_to_module;
981-
use crate::semantic_index::definition::{Definition, DefinitionKind};
980+
use crate::semantic_index::definition::{Definition, DefinitionKind, module_docstring};
982981
use crate::semantic_index::scope::{NodeWithScopeKind, ScopeId};
983982
use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map};
984983
use crate::{Db, ModuleName, resolve_module, resolve_real_module};
@@ -992,6 +991,8 @@ mod resolve_definition {
992991
pub enum ResolvedDefinition<'db> {
993992
/// The import resolved to a specific definition within a module
994993
Definition(Definition<'db>),
994+
/// The import resolved to an entire module
995+
Module(File),
995996
/// The import resolved to a file with a specific range
996997
FileWithRange(FileRange),
997998
}
@@ -1000,9 +1001,18 @@ mod resolve_definition {
10001001
fn file(&self, db: &'db dyn Db) -> File {
10011002
match self {
10021003
ResolvedDefinition::Definition(definition) => definition.file(db),
1004+
ResolvedDefinition::Module(file) => *file,
10031005
ResolvedDefinition::FileWithRange(file_range) => file_range.file(),
10041006
}
10051007
}
1008+
1009+
pub fn docstring(&self, db: &'db dyn Db) -> Option<String> {
1010+
match self {
1011+
ResolvedDefinition::Definition(definition) => definition.docstring(db),
1012+
ResolvedDefinition::Module(file) => module_docstring(db, *file),
1013+
ResolvedDefinition::FileWithRange(_) => None,
1014+
}
1015+
}
10061016
}
10071017

10081018
/// Resolve import definitions to their targets.
@@ -1070,10 +1080,7 @@ mod resolve_definition {
10701080

10711081
// For simple imports like "import os", we want to navigate to the module itself.
10721082
// Return the module file directly instead of trying to find definitions within it.
1073-
vec![ResolvedDefinition::FileWithRange(FileRange::new(
1074-
module_file,
1075-
TextRange::default(),
1076-
))]
1083+
vec![ResolvedDefinition::Module(module_file)]
10771084
}
10781085

10791086
DefinitionKind::ImportFrom(import_from_def) => {
@@ -1283,24 +1290,19 @@ mod resolve_definition {
12831290
}
12841291
trace!("Built Definition Path: {path:?}");
12851292
}
1286-
ResolvedDefinition::FileWithRange(file_range) => {
1287-
return if file_range.range() == TextRange::default() {
1288-
trace!(
1289-
"Found module mapping: {} => {}",
1290-
stub_file.path(db),
1291-
real_file.path(db)
1292-
);
1293-
// This is just a reference to a module, no need to do paths
1294-
Some(vec![ResolvedDefinition::FileWithRange(FileRange::new(
1295-
real_file,
1296-
TextRange::default(),
1297-
))])
1298-
} else {
1299-
// Not yet implemented -- in this case we want to recover something like a Definition
1300-
// and build a Definition Path, but this input is a bit too abstract for now.
1301-
trace!("Found arbitrary FileWithRange by stub mapping, giving up");
1302-
None
1303-
};
1293+
ResolvedDefinition::Module(_) => {
1294+
trace!(
1295+
"Found module mapping: {} => {}",
1296+
stub_file.path(db),
1297+
real_file.path(db)
1298+
);
1299+
return Some(vec![ResolvedDefinition::Module(real_file)]);
1300+
}
1301+
ResolvedDefinition::FileWithRange(_) => {
1302+
// Not yet implemented -- in this case we want to recover something like a Definition
1303+
// and build a Definition Path, but this input is a bit too abstract for now.
1304+
trace!("Found arbitrary FileWithRange while stub mapping, giving up");
1305+
return None;
13041306
}
13051307
}
13061308

0 commit comments

Comments
 (0)