From 7ac1874ca066123a64e7b8b49e55a80c0303a100 Mon Sep 17 00:00:00 2001 From: RasmusNygren Date: Sat, 27 Dec 2025 14:29:45 +0100 Subject: [PATCH] [ty] Use the AST to suppress keywords in decorators (#22224) --- crates/ty_ide/src/completion.rs | 90 ++++++++++++++------------------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 71c7952fcd..ba595d6e6c 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -770,10 +770,22 @@ impl<'m> ContextCursor<'m> { /// Returns None if no context-based exclusions can /// be identified. Meaning that all keywords are valid. fn valid_keywords(&self) -> Option> { - if self.is_in_decorator_expression() { + let covering_node = self.covering_node(self.range); + + // Check if the cursor is within the naming + // part of a decorator node. + if covering_node + .ancestors() + // We bail if we're specifying arguments as we don't + // want to suppress suggestions there. + .take_while(|node| { + !matches!(node, ast::AnyNodeRef::Arguments(_)) && !node.is_statement() + }) + .any(|node| matches!(node, ast::AnyNodeRef::Decorator(_))) + { return Some(FxHashSet::from_iter(["lambda"])); } - self.covering_node(self.range).ancestors().find_map(|node| { + covering_node.ancestors().find_map(|node| { self.is_in_for_statement_iterable(node) .then(|| FxHashSet::from_iter(["yield", "lambda", "await"])) .or_else(|| { @@ -787,51 +799,6 @@ impl<'m> ContextCursor<'m> { }) } - /// Returns true if the cursor is after an `@` token - /// that corresponds to a decorator declaration - /// - /// `@` can also be used as an operator, this distinguishes - /// between the two usages and only looks for the decorator case. - fn is_in_decorator_expression(&self) -> bool { - const LIMIT: usize = 10; - enum S { - Start, - At, - } - let mut state = S::Start; - for token in self.tokens_before.iter().rev().take(LIMIT) { - // Matches lines that starts with `@` as - // heuristic for decorators. When decorators - // are constructed they are often not identified - // as decorators yet by the AST, hence we use - // token matching for the decorator case. - // - // As the grammar also allows @ to be used as an operator, - // we want to distinguish between whether it looks - // like it's being used as an operator or a - // decorator. - // - // TODO: This doesn't handle decorators - // that start at the very top of the file. - state = match (state, token.kind()) { - (S::Start, TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent) => break, - (S::Start, TokenKind::At) => S::At, - (S::Start, _) => S::Start, - ( - S::At, - TokenKind::Newline - | TokenKind::NonLogicalNewline - | TokenKind::Indent - | TokenKind::Dedent, - ) => { - return true; - } - _ => break, - } - } - false - } - /// Returns true when only an expression is valid after the cursor /// according to the python grammar. /// @@ -6652,9 +6619,6 @@ if x in a: .not_contains("raise"); } - // TODO: This should not contain raise. - // `is_in_decorator_expression` currently doesn't - // detect decorators that start at the top of the file. #[test] fn only_lambda_keyword_in_decorator_top_of_file() { completion_test_builder( @@ -6665,7 +6629,7 @@ def func(): ... ) .build() .contains("lambda") - .contains("raise"); + .not_contains("raise"); } #[test] @@ -6699,6 +6663,30 @@ from dataclasses import dataclass .contains("frozen"); } + #[test] + fn decorator_args_do_not_suppress_keywords() { + completion_test_builder( + "\ +from dataclasses import dataclass + +@dataclass(frozen=Tr +", + ) + .build() + .contains("True"); + } + + #[test] + fn decorator_chained_call_args_do_not_suppress_keywords() { + completion_test_builder( + "\ + @decorator(foo=False)(bar=Tr + ", + ) + .build() + .contains("True"); + } + #[test] fn statement_keywords_in_if_body() { completion_test_builder(