-
Notifications
You must be signed in to change notification settings - Fork 2k
[red-knot] Add inlay type hints #17214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
51f1bc4
Initial progress in playground
MatthewMckee4 9a98afb
Remove redundant calls
MatthewMckee4 b110361
Fix build and filter some more types
MatthewMckee4 978cc48
Refactory and add more filter logic
MatthewMckee4 cb898fe
Fix build
MatthewMckee4 41f583e
Add knot server functionality for inlay hints
MatthewMckee4 0da803d
Add InlayHintVisitor to visit only the statements we want
MatthewMckee4 135d8a6
Add tests
MatthewMckee4 00eeaf5
Merge branch 'main' into inlay-hints
MatthewMckee4 bdb75b1
Fix build
MatthewMckee4 52106bc
Revert "Fix build"
MatthewMckee4 a135a6d
Fix build
MatthewMckee4 2d6b3d5
Fix build
MatthewMckee4 fad5abb
Add correct tuple inlay hints and update tests
MatthewMckee4 cef34a5
Add option to opt in to inlay hints
MatthewMckee4 c1c5434
run generate-all
MatthewMckee4 08cfce7
Update inlay_hints.rs
MatthewMckee4 d1d071a
Merge branch 'main' into inlay-hints
MatthewMckee4 313f76f
Apply changes as per review
MatthewMckee4 350ff11
run generate-all
MatthewMckee4 85377eb
Changes per review
MatthewMckee4 808a03a
Merge branch 'main' into inlay-hints
MatthewMckee4 b901496
Fix playground
MatthewMckee4 b2bc7d2
Update per review
MatthewMckee4 9e66656
Merge branch 'main' into inlay-hints
MatthewMckee4 e438ffa
Simplify tests
MichaReiser 70fb3e1
Show hint for all `ExprName::Store` locations
MichaReiser 59f3c7f
Extract helper in wasm code
MichaReiser File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| use crate::Db; | ||
| use red_knot_python_semantic::types::Type; | ||
| use red_knot_python_semantic::{HasType, SemanticModel}; | ||
| use ruff_db::files::File; | ||
| use ruff_db::parsed::parsed_module; | ||
| use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; | ||
| use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; | ||
| use ruff_text_size::{Ranged, TextRange, TextSize}; | ||
| use std::fmt; | ||
| use std::fmt::Formatter; | ||
|
|
||
| #[derive(Debug, Clone, Eq, PartialEq)] | ||
| pub struct InlayHint<'db> { | ||
| pub position: TextSize, | ||
| pub content: InlayHintContent<'db>, | ||
| } | ||
|
|
||
| impl<'db> InlayHint<'db> { | ||
| pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { | ||
| self.content.display(db) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Clone, Eq, PartialEq)] | ||
| pub enum InlayHintContent<'db> { | ||
| Type(Type<'db>), | ||
| ReturnType(Type<'db>), | ||
| } | ||
|
|
||
| impl<'db> InlayHintContent<'db> { | ||
| pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> { | ||
| DisplayInlayHint { db, hint: self } | ||
| } | ||
| } | ||
|
|
||
| pub struct DisplayInlayHint<'a, 'db> { | ||
| db: &'db dyn Db, | ||
| hint: &'a InlayHintContent<'db>, | ||
| } | ||
|
|
||
| impl fmt::Display for DisplayInlayHint<'_, '_> { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { | ||
| match self.hint { | ||
| InlayHintContent::Type(ty) => { | ||
| write!(f, ": {}", ty.display(self.db.upcast())) | ||
| } | ||
| InlayHintContent::ReturnType(ty) => { | ||
| write!(f, " -> {}", ty.display(self.db.upcast())) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'_>> { | ||
| let mut visitor = InlayHintVisitor::new(db, file, range); | ||
|
|
||
| let ast = parsed_module(db.upcast(), file); | ||
|
|
||
| visitor.visit_body(ast.suite()); | ||
|
|
||
| visitor.hints | ||
| } | ||
|
|
||
| struct InlayHintVisitor<'db> { | ||
| model: SemanticModel<'db>, | ||
| hints: Vec<InlayHint<'db>>, | ||
| in_assignment: bool, | ||
| range: TextRange, | ||
| } | ||
|
|
||
| impl<'db> InlayHintVisitor<'db> { | ||
| fn new(db: &'db dyn Db, file: File, range: TextRange) -> Self { | ||
| Self { | ||
| model: SemanticModel::new(db.upcast(), file), | ||
| hints: Vec::new(), | ||
| in_assignment: false, | ||
| range, | ||
| } | ||
| } | ||
|
|
||
| fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) { | ||
| self.hints.push(InlayHint { | ||
| position, | ||
| content: InlayHintContent::Type(ty), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> { | ||
| fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { | ||
| if self.range.intersect(node.range()).is_some() { | ||
| TraversalSignal::Traverse | ||
| } else { | ||
| TraversalSignal::Skip | ||
| } | ||
| } | ||
|
|
||
| fn visit_stmt(&mut self, stmt: &Stmt) { | ||
| let node = AnyNodeRef::from(stmt); | ||
|
|
||
| if !self.enter_node(node).is_traverse() { | ||
| return; | ||
| } | ||
|
|
||
|
MichaReiser marked this conversation as resolved.
|
||
| match stmt { | ||
| Stmt::Assign(assign) => { | ||
| self.in_assignment = true; | ||
| for target in &assign.targets { | ||
| self.visit_expr(target); | ||
| } | ||
| self.in_assignment = false; | ||
|
|
||
| return; | ||
| } | ||
| // TODO | ||
| Stmt::FunctionDef(_) => {} | ||
| Stmt::For(_) => {} | ||
| Stmt::Expr(_) => { | ||
| // Don't traverse into expression statements because we don't show any hints. | ||
| return; | ||
| } | ||
| _ => {} | ||
| } | ||
|
|
||
| source_order::walk_stmt(self, stmt); | ||
| } | ||
|
|
||
| fn visit_expr(&mut self, expr: &'_ Expr) { | ||
| if !self.in_assignment { | ||
| return; | ||
| } | ||
|
|
||
| match expr { | ||
| Expr::Name(name) => { | ||
| if name.ctx.is_store() { | ||
| let ty = expr.inferred_type(&self.model); | ||
| self.add_type_hint(expr.range().end(), ty); | ||
| } | ||
| } | ||
| _ => { | ||
| source_order::walk_expr(self, expr); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| use insta::assert_snapshot; | ||
| use ruff_db::{ | ||
| files::{system_path_to_file, File}, | ||
| source::source_text, | ||
| }; | ||
| use ruff_text_size::TextSize; | ||
|
|
||
| use crate::db::tests::TestDb; | ||
|
|
||
| use red_knot_python_semantic::{ | ||
| Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings, | ||
| }; | ||
| use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; | ||
| use ruff_python_ast::PythonVersion; | ||
|
|
||
| pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest { | ||
| const START: &str = "<START>"; | ||
| const END: &str = "<END>"; | ||
|
|
||
| let mut db = TestDb::new(); | ||
|
|
||
| let start = source.find(START); | ||
| let end = source | ||
| .find(END) | ||
| .map(|x| if start.is_some() { x - START.len() } else { x }) | ||
| .unwrap_or(source.len()); | ||
|
|
||
| let range = TextRange::new( | ||
| TextSize::try_from(start.unwrap_or_default()).unwrap(), | ||
| TextSize::try_from(end).unwrap(), | ||
| ); | ||
|
|
||
| let source = source.replace(START, ""); | ||
| let source = source.replace(END, ""); | ||
|
|
||
| db.write_file("main.py", source) | ||
| .expect("write to memory file system to be successful"); | ||
|
|
||
| let file = system_path_to_file(&db, "main.py").expect("newly written file to existing"); | ||
|
|
||
| Program::from_settings( | ||
| &db, | ||
| ProgramSettings { | ||
| python_version: PythonVersion::latest(), | ||
| python_platform: PythonPlatform::default(), | ||
| search_paths: SearchPathSettings { | ||
| extra_paths: vec![], | ||
| src_roots: vec![SystemPathBuf::from("/")], | ||
| custom_typeshed: None, | ||
| python_path: PythonPath::KnownSitePackages(vec![]), | ||
| }, | ||
| }, | ||
| ) | ||
| .expect("Default settings to be valid"); | ||
|
|
||
| InlayHintTest { db, file, range } | ||
| } | ||
|
|
||
| pub(super) struct InlayHintTest { | ||
| pub(super) db: TestDb, | ||
| pub(super) file: File, | ||
| pub(super) range: TextRange, | ||
| } | ||
|
|
||
| impl InlayHintTest { | ||
| fn inlay_hints(&self) -> String { | ||
| let hints = inlay_hints(&self.db, self.file, self.range); | ||
|
|
||
| let mut buf = source_text(&self.db, self.file).as_str().to_string(); | ||
|
|
||
| let mut offset = 0; | ||
|
|
||
| for hint in hints { | ||
| let end_position = (hint.position.to_u32() as usize) + offset; | ||
| let hint_str = format!("[{}]", hint.display(&self.db)); | ||
| buf.insert_str(end_position, &hint_str); | ||
| offset += hint_str.len(); | ||
| } | ||
|
|
||
| buf | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_assign_statement() { | ||
| let test = inlay_hint_test("x = 1"); | ||
|
|
||
| assert_snapshot!(test.inlay_hints(), @r" | ||
| x[: Literal[1]] = 1 | ||
| "); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tuple_assignment() { | ||
| let test = inlay_hint_test("x, y = (1, 'abc')"); | ||
|
|
||
| assert_snapshot!(test.inlay_hints(), @r#" | ||
| x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc') | ||
| "#); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_nested_tuple_assignment() { | ||
| let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))"); | ||
|
|
||
| assert_snapshot!(test.inlay_hints(), @r#" | ||
| x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2)) | ||
| "#); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_assign_statement_with_type_annotation() { | ||
| let test = inlay_hint_test("x: int = 1"); | ||
|
|
||
| assert_snapshot!(test.inlay_hints(), @r" | ||
| x: int = 1 | ||
| "); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_assign_statement_out_of_range() { | ||
| let test = inlay_hint_test("<START>x = 1<END>\ny = 2"); | ||
|
|
||
| assert_snapshot!(test.inlay_hints(), @r" | ||
| x[: Literal[1]] = 1 | ||
| y = 2 | ||
| "); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| mod diagnostic; | ||
| mod goto_type_definition; | ||
| mod hover; | ||
| mod inlay_hints; | ||
|
|
||
| pub(super) use diagnostic::DocumentDiagnosticRequestHandler; | ||
| pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; | ||
| pub(super) use hover::HoverRequestHandler; | ||
| pub(super) use inlay_hints::InlayHintRequestHandler; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.