diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 07e5816a9c..98626d9f67 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -2914,21 +2914,27 @@ impl<'src> Parser<'src> { self.current_token_range(), ); - // TODO(dhruvmanila): It seems that this recovery drops all the parsed - // decorators. Maybe we could convert them into statement expression - // with a flag indicating that this expression is part of a decorator. - // It's only possible to keep them if it's a function or class definition. - // We could possibly keep them if there's indentation error: - // - // ```python - // @decorator - // @decorator - // def foo(): ... - // ``` - // - // Or, parse it as a binary expression where the left side is missing. - // We would need to convert each decorator into a binary expression. - self.parse_statement() + let range = self.node_range(start); + + ast::StmtFunctionDef { + node_index: Default::default(), + range, + is_async: false, + decorator_list: decorators, + name: ast::Identifier { + id: Name::empty(), + range: self.missing_node_range(), + node_index: AtomicNodeIndex::NONE, + }, + type_params: None, + parameters: Box::new(ast::Parameters { + range: self.missing_node_range(), + ..ast::Parameters::default() + }), + returns: None, + body: vec![], + } + .into() } } } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap index dd7225493f..bec68a40f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/decorator_missing_expression.py --- ## AST @@ -10,6 +9,44 @@ Module( node_index: NodeIndex(None), range: 0..51, body: [ + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 0..4, + is_async: false, + decorator_list: [ + Decorator { + range: 0..4, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 1..4, + id: Name("def"), + ctx: Load, + }, + ), + }, + ], + name: Identifier { + id: Name(""), + range: 4..4, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 4..4, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [], + }, + ), AnnAssign( StmtAnnAssign { node_index: NodeIndex(None), diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap index a2e7b51517..83fd68468b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs -input_file: crates/ruff_python_parser/resources/inline/err/decorator_unexpected_token.py --- ## AST @@ -10,6 +9,44 @@ Module( node_index: NodeIndex(None), range: 0..34, body: [ + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 0..4, + is_async: false, + decorator_list: [ + Decorator { + range: 0..4, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 1..4, + id: Name("foo"), + ctx: Load, + }, + ), + }, + ], + name: Identifier { + id: Name(""), + range: 4..4, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 4..4, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [], + }, + ), With( StmtWith { node_index: NodeIndex(None), @@ -46,6 +83,44 @@ Module( ], }, ), + FunctionDef( + StmtFunctionDef { + node_index: NodeIndex(None), + range: 23..27, + is_async: false, + decorator_list: [ + Decorator { + range: 23..27, + node_index: NodeIndex(None), + expression: Name( + ExprName { + node_index: NodeIndex(None), + range: 24..27, + id: Name("foo"), + ctx: Load, + }, + ), + }, + ], + name: Identifier { + id: Name(""), + range: 27..27, + node_index: NodeIndex(None), + }, + type_params: None, + parameters: Parameters { + range: 27..27, + node_index: NodeIndex(None), + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [], + }, + ), Assign( StmtAssign { node_index: NodeIndex(None), diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index d8dabbf413..0a25cfb00e 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -48,6 +48,7 @@ pub fn completion<'db>( } ContextKind::NonImport(ref non_import) => { let model = SemanticModel::new(db, file); + dbg!(&non_import.target); let (semantic_completions, scoped) = match non_import.target { CompletionTargetAst::ObjectDot { expr } => { (model.attribute_completions(expr), None) @@ -520,7 +521,7 @@ impl<'m> Context<'m> { ContextKind::Import(import) } else { let target_token = CompletionTargetTokens::find(&cursor)?; - let target = target_token.ast(&cursor)?; + let target = dbg!(target_token.ast(&cursor)?); ContextKind::NonImport(ContextNonImport { target }) }; @@ -559,6 +560,7 @@ impl<'m> Context<'m> { /// The lifetime parameter `'m` refers to the shorter of the following /// lifetimes: the parsed module the cursor is in and the actual bytes /// making up the source file containing the cursor. + struct ContextCursor<'m> { /// The parsed module containing the cursor. parsed: &'m ParsedModuleRef, @@ -1461,7 +1463,7 @@ impl<'t> CompletionTargetTokens<'t> { } } CompletionTargetTokens::Generic { token } => { - let node = cursor.covering_node(token.range()).node(); + let node = dbg!(cursor.covering_node(dbg!(token.range()))).node(); Some(CompletionTargetAst::Scoped(ScopedTarget { node })) } CompletionTargetTokens::Unknown => { @@ -6686,6 +6688,19 @@ def func(): .not_contains("False"); } + #[test] + fn decorator_without_class_or_function() { + completion_test_builder( + "\ +from dataclasses import dataclass + +@dataclass(froz +", + ) + .build() + .contains("frozen"); + } + #[test] fn statement_keywords_in_if_body() { completion_test_builder( diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index addc3897de..f0ceacc7f1 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -442,8 +442,18 @@ impl Workspace { new_text: edit.content().map(ToString::to_string).unwrap_or_default(), } }); + + let name = comp.insert.as_deref().unwrap_or(&comp.name).to_string(); + let import_suffix = comp + .module_name + .and_then(|name| import_edit.is_some().then(|| format!(" (import {name})"))); + + let label = import_suffix + .map(|suffix| format!("{name}{suffix}")) + .unwrap_or_else(|| name); + Completion { - name: comp.name.into(), + label, kind, detail: type_display, module_name: comp.module_name.map(ToString::to_string), @@ -1070,7 +1080,7 @@ pub struct Hover { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Completion { #[wasm_bindgen(getter_with_clone)] - pub name: String, + pub label: String, pub kind: Option, #[wasm_bindgen(getter_with_clone)] pub insert_text: Option, diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index c104a93c63..df58877ded 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -336,7 +336,7 @@ class PlaygroundServer return { suggestions: completions.map((completion, i) => ({ label: { - label: completion.name, + label: completion.label, detail: completion.module_name == null ? undefined