Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ use crate::alt::attr::AttrInfo;
use crate::binding::binding::Binding;
use crate::binding::binding::Key;
use crate::binding::binding::KeyAnnotation;
use crate::binding::binding::KeyExport;
use crate::binding::metadata::BindingsMetadata;
use crate::config::error_kind::ErrorKind;
use crate::error::error::ErrorQuickFix;
Expand Down Expand Up @@ -1494,6 +1495,12 @@ impl<'a> Transaction<'a> {
context,
}) = self.identifier_at(handle, position)
else {
if let Some((_, name)) = self.get_exports_data(handle).dunder_all_name_at(position) {
return self
.get_solutions(handle)?
.get_hashed_opt(Hashed::new(&KeyExport(name)))
.cloned();
}
return self
.type_from_match_wildcard_at_impl(handle, position)
.or_else(|| self.type_from_expression_at_impl(handle, position, false));
Expand Down
61 changes: 57 additions & 4 deletions pyrefly/lib/test/lsp/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ fn get_test_report(state: &State, handle: &Handle, position: TextSize) -> String
}
}

// BUG: Hover shows string literal types for __all__ and __slots__ entries.
// It should show (variable) foo: int and (attribute) bar: str (issue #4344).
// BUG: Hover shows a string literal type for the __slots__ entry (issue #4344).
#[test]
fn hover_symbol_literals() {
let code = r#"
Expand All @@ -58,14 +57,22 @@ class C:
def __init__(self) -> None:
self.bar: str = ""
"#;
let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report);
let report = get_batched_lsp_operations_report(&[("main", code)], |state, handle, position| {
match get_hover(&state.transaction(), handle, position, false) {
Some(Hover {
contents: Contents::MarkupContent(markup),
..
}) => markup.value,
_ => "None".to_owned(),
}
});
assert_eq!(
r#"
# main.py
3 | __all__ = ["foo"]
^
```python
(variable) foo: Literal['foo']
(variable) foo: int
```

7 | __slots__ = ("bar",)
Expand All @@ -79,6 +86,52 @@ Literal['bar']
);
}

#[test]
fn hover_dunder_all_reexport_entries() {
let code = r#"
from lib import foo
__all__ = ["foo"]
# ^
__all__.append("foo")
# ^
__all__.extend(["foo"])
# ^
__all__.remove("foo")
# ^
"#;
get_batched_lsp_operations_report(
&[("main", code), ("lib", "foo: int = 1\n")],
|state, handle, position| {
let report = get_test_report(state, handle, position);
assert!(report.contains("(variable) foo: int"), "got: {report}");
report
},
);
}

#[test]
fn hover_dunder_all_class_entry() {
let code = r#"
class C: ...
__all__ = ["C"]
# ^
"#;
let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report);
assert!(report.contains("(class) C: type[C]"), "got: {report}");
}

#[test]
fn hover_string_matching_dunder_all_entry() {
let code = r#"
foo: int = 1
value = "foo"
# ^
__all__ = ["foo"]
"#;
let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report);
assert!(report.contains("Literal['foo']"), "got: {report}");
}

fn get_test_report_at_verbosity(
state: &State,
handle: &Handle,
Expand Down
63 changes: 63 additions & 0 deletions pyrefly/lib/test/lsp/lsp_interaction/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,76 @@
* LICENSE file in the root directory of this source tree.
*/

use std::fs;

use lsp_types::Uri;
use pyrefly_lsp_test::object_model::InitializeSettings;
use pyrefly_lsp_test::object_model::LspInteraction;
use serde_json::json;
use tempfile::TempDir;

use crate::test::lsp::lsp_interaction::util::get_test_files_root;

#[test]
fn hover_dunder_all_uses_open_buffer_export() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("main.pyi"), "value: bytes\n").unwrap();
fs::write(
root.path().join("main.py"),
"value = 1\n__all__ = ['value']\n",
)
.unwrap();
let mut interaction = LspInteraction::new();
interaction.set_root(root.path().to_path_buf());
interaction.initialize(Default::default()).unwrap();
interaction.client.did_open("main.py");
interaction.client.did_change(
"main.py",
"\nvalue = 1\nvalue = 'final'\n__all__ = ['value']\n",
);
interaction
.client
.hover("main.py", 3, 13)
.expect_hover_response_with_markup(|value| {
value.is_some_and(|text| text.contains("(variable) value: str"))
})
.unwrap();
interaction.shutdown().unwrap();
}

#[test]
fn hover_dunder_all_uses_unopened_dependency_export() {
for with_stub in [false, true] {
let root = TempDir::new().unwrap();
fs::write(root.path().join("dep.py"), "value: int = 1\n").unwrap();
if with_stub {
fs::write(root.path().join("dep.pyi"), "value: str\n").unwrap();
}
fs::write(
root.path().join("main.py"),
"from dep import value as public\n__all__ = ['public']\n",
)
.unwrap();
let mut interaction = LspInteraction::new();
interaction.set_root(root.path().to_path_buf());
interaction.initialize(Default::default()).unwrap();
interaction.client.did_open("main.py");
let expected = if with_stub {
"value: str"
} else {
"value: int"
};
interaction
.client
.hover("main.py", 1, 13)
.expect_hover_response_with_markup(|value| {
value.is_some_and(|text| text.contains(expected))
})
.unwrap();
interaction.shutdown().unwrap();
}
}

#[test]
fn test_hover_basic() {
let root = get_test_files_root();
Expand Down
23 changes: 23 additions & 0 deletions pyrefly/lib/test/lsp/type_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ fn get_test_report(state: &State, handle: &Handle, position: TextSize) -> String
}
}

#[test]
fn dunder_all_entry_uses_exported_type_definition() {
let code = r#"
class Exported: ...
value: Exported = Exported()
__all__ = ["value"]
# ^
"#;
let report = get_batched_lsp_operations_report(&[("main", code)], get_test_report);
assert_eq!(
r#"
# main.py
4 | __all__ = ["value"]
^
Type Definition Result:
2 | class Exported: ...
^^^^^^^^
"#
.trim(),
report.trim(),
);
}

#[test]
fn function_goes_to_its_own_def() {
let code = r#"
Expand Down
Loading