Compare commits

...

1 Commits

Author SHA1 Message Date
Aria Desires
6caa37fb27 support rendering __doc__ on hover 2025-12-18 01:11:15 -05:00
2 changed files with 106 additions and 1 deletions

View File

@@ -2739,6 +2739,86 @@ def function():
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_dunder_doc() {
let test = cursor_test(
r#"
class My<CURSOR>Class:
__doc__ = "hello there"
"#,
);
assert_snapshot!(test.hover(), @r#"
<class 'MyClass'>
---------------------------------------------
hello there
---------------------------------------------
```xml
<class 'MyClass'>
```
---
hello there
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:7
|
2 | class MyClass:
| ^^-^^^^
| | |
| | Cursor offset
| source
3 | __doc__ = "hello there"
|
"#);
}
#[test]
fn hover_dunder_doc_complex() {
let test = cursor_test(
r#"
class My<CURSOR>Class:
__doc__ = (
r"""This is some extremely complex docstring
Designed to make
"""
+ r"""
A typechecker
"""
"""
Fall to its knees and sob
"""
r"""
Witness my works and
weep before them
"""
)
"#,
);
assert_snapshot!(test.hover(), @r#"
<class 'MyClass'>
---------------------------------------------
```xml
<class 'MyClass'>
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:7
|
2 | class MyClass:
| ^^-^^^^
| | |
| | Cursor offset
| source
3 | __doc__ = (
4 | r"""This is some extremely complex docstring
|
"#);
}
#[test]
fn hover_class_typevar_variance() {
let test = cursor_test(

View File

@@ -2,7 +2,7 @@ use std::ops::Deref;
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::Db;
@@ -133,6 +133,10 @@ pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option<String> {
/// Extract a docstring from a function, module, or class body.
fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
docstring_from_body_normal(body).or_else(|| docstring_from_body_dunder_doc(body))
}
fn docstring_from_body_normal(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
let stmt = body.first()?;
// Require the docstring to be a standalone expression.
let ast::Stmt::Expr(ast::StmtExpr {
@@ -147,6 +151,27 @@ fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
value.as_string_literal_expr()
}
fn docstring_from_body_dunder_doc(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
for stmt in body {
let Some(stmt) = stmt.as_assign_stmt() else {
continue;
};
let [Expr::Name(name)] = &stmt.targets[..] else {
continue;
};
if name.id.as_str() != "__doc__" {
continue;
}
let Expr::StringLiteral(literal) = &*stmt.value else {
continue;
};
return Some(literal);
}
None
}
/// One or more [`Definition`]s.
#[derive(Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub struct Definitions<'db> {