From 0847d7a168d6684eea74fc38ea2e593b775a556c Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:41:11 +0000 Subject: [PATCH] More parser error recovery, unlocking completions in more locations --- compiler/qsc_frontend/src/typeck/tests.rs | 2 +- compiler/qsc_parse/src/expr.rs | 34 ++- compiler/qsc_parse/src/item.rs | 83 +++++-- compiler/qsc_parse/src/item/tests.rs | 152 ++++++++++-- compiler/qsc_parse/src/prim.rs | 69 +++++- compiler/qsc_parse/src/prim/tests.rs | 25 +- compiler/qsc_parse/src/stmt.rs | 38 ++- compiler/qsc_parse/src/stmt/tests.rs | 10 +- compiler/qsc_parse/src/ty.rs | 16 +- language_service/src/completion/tests.rs | 268 ++++++++++++++++++++++ 10 files changed, 622 insertions(+), 75 deletions(-) diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs index df5d0e8e47..68e1fbdbc0 100644 --- a/compiler/qsc_frontend/src/typeck/tests.rs +++ b/compiler/qsc_frontend/src/typeck/tests.rs @@ -4555,7 +4555,7 @@ fn expr_incomplete_field_access_no_semi() { &expect![[r##" #14 55-57 "()" : Unit #18 65-99 "{\n (new B { C = 5 }).\n }" : ? - #20 75-93 "(new B { C = 5 })." : ? + #20 75-98 "(new B { C = 5 }).\n " : ? #21 75-92 "(new B { C = 5 })" : UDT<"B": Item 1> #22 76-91 "new B { C = 5 }" : UDT<"B": Item 1> #27 88-89 "5" : Int diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs index 62d1eefbe2..433dcf2d55 100644 --- a/compiler/qsc_parse/src/expr.rs +++ b/compiler/qsc_parse/src/expr.rs @@ -14,7 +14,10 @@ use crate::{ ClosedBinOp, Delim, InterpolatedEnding, InterpolatedStart, Radix, StringToken, Token, TokenKind, }, - prim::{ident, opt, pat, recovering_path, recovering_token, seq, shorten, token}, + prim::{ + ident, opt, parse_or_else, pat, recovering_parse_or_else, recovering_path, seq, shorten, + token, + }, scan::ParserContext, stmt, Error, ErrorKind, Result, }; @@ -247,22 +250,23 @@ fn expr_base(s: &mut ParserContext) -> Result> { fn recovering_struct(s: &mut ParserContext) -> Result> { let name = recovering_path(s, WordKinds::PathStruct)?; - if let Err(e) = token(s, TokenKind::Open(Delim::Brace)) { - s.push_error(e); - return Ok(Box::new(ExprKind::Struct(name, None, Box::new([])))); - } + let (copy, fields) = recovering_parse_or_else( + s, + |_| (None, Box::new([])), + &[TokenKind::Close(Delim::Brace)], + struct_fields, + ); - let (copy, fields) = struct_fields(s)?; - recovering_token(s, TokenKind::Close(Delim::Brace)); Ok(Box::new(ExprKind::Struct(name, copy, fields))) } /// A sequence of field assignments and an optional base expression, -/// e.g. `...a, b = c, d = e` +/// e.g. `{ ...a, b = c, d = e }` #[allow(clippy::type_complexity)] fn struct_fields( s: &mut ParserContext<'_>, ) -> Result<(Option>, Box<[Box]>)> { + token(s, TokenKind::Open(Delim::Brace))?; let copy: Option> = opt(s, |s| { token(s, TokenKind::DotDotDot)?; expr(s) @@ -271,6 +275,7 @@ fn struct_fields( if copy.is_none() || copy.is_some() && token(s, TokenKind::Comma).is_ok() { (fields, _) = seq(s, parse_field_assign)?; } + token(s, TokenKind::Close(Delim::Brace))?; Ok((copy, fields.into_boxed_slice())) } @@ -714,17 +719,8 @@ fn lambda_op(s: &mut ParserContext, input: Expr, kind: CallableKind) -> Result) -> Result> { s.expect(WordKinds::Field); - let expr = ExprKind::Field( - lhs, - match ident(s) { - Ok(i) => FieldAccess::Ok(i), - Err(e) => { - s.push_error(e); - FieldAccess::Err - } - }, - ); - Ok(Box::new(expr)) + let field_access = parse_or_else(s, |_| FieldAccess::Err, |s| Ok(FieldAccess::Ok(ident(s)?)))?; + Ok(Box::new(ExprKind::Field(lhs, field_access))) } fn index_op(s: &mut ParserContext, lhs: Box) -> Result> { diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs index 0ebe54865b..24cf7c0071 100644 --- a/compiler/qsc_parse/src/item.rs +++ b/compiler/qsc_parse/src/item.rs @@ -15,7 +15,7 @@ use super::{ prim::{ident, many, opt, pat, seq, token}, scan::ParserContext, stmt, - ty::{self, ty}, + ty::{self, recovering_ty, ty}, Error, Result, }; @@ -23,17 +23,18 @@ use crate::{ completion::WordKinds, lex::{ClosedBinOp, Delim, TokenKind}, prim::{ - barrier, path, recovering, recovering_path, recovering_semi, recovering_token, shorten, + barrier, parse_or_else, path, recovering, recovering_path, recovering_semi, + recovering_token, shorten, }, stmt::check_semis, ty::array_or_arrow, ErrorKind, }; use qsc_ast::ast::{ - Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents, + Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, FunctorExpr, Ident, Idents, ImportOrExportDecl, ImportOrExportItem, Item, ItemKind, Namespace, NodeId, Pat, PatKind, Path, - PathKind, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef, - TyDefKind, TyKind, + PathKind, Spec, SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, StructDecl, TopLevelNode, Ty, + TyDef, TyDefKind, TyKind, }; use qsc_data_structures::language_features::LanguageFeatures; use qsc_data_structures::span::Span; @@ -118,7 +119,23 @@ pub(super) fn parse_namespaces(s: &mut ParserContext) -> Result> } pub(super) fn parse_top_level_nodes(s: &mut ParserContext) -> Result> { - let nodes = many(s, parse_top_level_node)?; + const RECOVERY_TOKENS: &[TokenKind] = &[TokenKind::Semi, TokenKind::Close(Delim::Brace)]; + let nodes = { + many(s, |s| { + recovering( + s, + |span| { + TopLevelNode::Stmt(Box::new(Stmt { + id: NodeId::default(), + span, + kind: Box::new(StmtKind::Err), + })) + }, + RECOVERY_TOKENS, + parse_top_level_node, + ) + }) + }?; recovering_token(s, TokenKind::Eof); Ok(nodes) } @@ -498,16 +515,35 @@ fn parse_callable_decl(s: &mut ParserContext) -> Result> { let input = pat(s)?; check_input_parens(&input)?; - token(s, TokenKind::Colon)?; - throw_away_doc(s); - let output = ty(s)?; - let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() { - Some(Box::new(ty::functor_expr(s)?)) - } else { - None - }; + + let (output, functors) = parse_or_else( + s, + |span| { + ( + Box::new(Ty { + id: NodeId::default(), + span, + kind: Box::new(TyKind::Err), + }), + None, + ) + }, + parse_callable_output_and_functors, + )?; + throw_away_doc(s); - let body = parse_callable_body(s)?; + + let body = parse_or_else( + s, + |span| { + CallableBody::Block(Box::new(Block { + id: NodeId::default(), + span, + stmts: Box::default(), + })) + }, + parse_callable_body, + )?; Ok(Box::new(CallableDecl { id: NodeId::default(), @@ -516,12 +552,27 @@ fn parse_callable_decl(s: &mut ParserContext) -> Result> { name, generics: generics.into_boxed_slice(), input, - output: Box::new(output), + output, functors, body: Box::new(body), })) } +/// The output and functors part of the callable signature, e.g. `: Unit is Adj` +fn parse_callable_output_and_functors( + s: &mut ParserContext, +) -> Result<(Box, Option>)> { + token(s, TokenKind::Colon)?; + throw_away_doc(s); + let output = recovering_ty(s)?; + let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() { + Some(Box::new(ty::functor_expr(s)?)) + } else { + None + }; + Ok((output.into(), functors)) +} + fn parse_callable_body(s: &mut ParserContext) -> Result { let lo = s.peek().span.lo; token(s, TokenKind::Open(Delim::Brace))?; diff --git a/compiler/qsc_parse/src/item/tests.rs b/compiler/qsc_parse/src/item/tests.rs index 42ad21731e..642f93f8f0 100644 --- a/compiler/qsc_parse/src/item/tests.rs +++ b/compiler/qsc_parse/src/item/tests.rs @@ -3,7 +3,7 @@ use super::{ parse, parse_attr, parse_implicit_namespace, parse_import_or_export, parse_open, - parse_spec_decl, + parse_spec_decl, parse_top_level_nodes, }; use crate::{ scan::ParserContext, @@ -927,19 +927,28 @@ fn function_missing_output_ty() { parse, "function Foo() { body intrinsic; }", &expect![[r#" - Error( - Token( - Colon, - Open( - Brace, + Item _id_ [0-34]: + Callable _id_ [0-34] (Function): + name: Ident _id_ [9-12] "Foo" + input: Pat _id_ [12-14]: Unit + output: Type _id_ [15-15]: Err + body: Specializations: + SpecDecl _id_ [17-32] (Body): Gen: Intrinsic + + [ + Error( + Token( + Colon, + Open( + Brace, + ), + Span { + lo: 15, + hi: 16, + }, ), - Span { - lo: 15, - hi: 16, - }, ), - ) - "#]], + ]"#]], ); } @@ -1405,7 +1414,12 @@ fn recover_callable_item() { body: Block: Block _id_ [47-52]: Stmt _id_ [49-50]: Expr: Expr _id_ [49-50]: Lit: Int(5) Item _id_ [65-86]: - Err + Callable _id_ [65-86] (Function): + name: Ident _id_ [74-77] "Bar" + input: Pat _id_ [77-79]: Unit + output: Type _id_ [80-80]: Err + body: Block: Block _id_ [80-86]: + Stmt _id_ [82-84]: Expr: Expr _id_ [82-84]: Lit: Int(10) Item _id_ [99-131]: Callable _id_ [99-131] (Operation): name: Ident _id_ [109-112] "Baz" @@ -2252,3 +2266,115 @@ fn missing_semi_between_items() { ]"#]], ); } + +#[test] +fn callable_decl_no_return_type_or_body_recovery() { + check( + parse, + "operation Foo<'T>() : ", + &expect![[r#" + Item _id_ [0-22]: + Callable _id_ [0-22] (Operation): + name: Ident _id_ [10-13] "Foo" + generics: + Ident _id_ [14-16] "'T" + input: Pat _id_ [17-19]: Unit + output: Type _id_ [22-22]: Err + body: Block: Block _id_ [22-22]: + + [ + Error( + Rule( + "type", + Eof, + Span { + lo: 22, + hi: 22, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn callable_decl_broken_return_type_no_body_recovery() { + check( + parse, + "operation Foo<'T>() : () => ", + &expect![[r#" + Item _id_ [0-28]: + Callable _id_ [0-28] (Operation): + name: Ident _id_ [10-13] "Foo" + generics: + Ident _id_ [14-16] "'T" + input: Pat _id_ [17-19]: Unit + output: Type _id_ [22-28]: Arrow (Operation): + param: Type _id_ [22-24]: Unit + return: Type _id_ [28-28]: Err + body: Block: Block _id_ [28-28]: + + [ + Error( + Rule( + "type", + Eof, + Span { + lo: 28, + hi: 28, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn top_level_nodes() { + check_vec( + parse_top_level_nodes, + "function Foo() : Unit { body intrinsic; } let x = 5;", + &expect![[r#" + Stmt _id_ [0-41]: Item: Item _id_ [0-41]: + Callable _id_ [0-41] (Function): + name: Ident _id_ [9-12] "Foo" + input: Pat _id_ [12-14]: Unit + output: Type _id_ [17-21]: Path: Path _id_ [17-21] (Ident _id_ [17-21] "Unit") + body: Specializations: + SpecDecl _id_ [24-39] (Body): Gen: Intrinsic, + Stmt _id_ [42-52]: Local (Immutable): + Pat _id_ [46-47]: Bind: + Ident _id_ [46-47] "x" + Expr _id_ [50-51]: Lit: Int(5)"#]], + ); +} + +#[test] +fn top_level_nodes_error_recovery() { + check_vec( + parse_top_level_nodes, + "function Foo() : Unit { body intrinsic; } 3 + ", + &expect![[r#" + Stmt _id_ [0-41]: Item: Item _id_ [0-41]: + Callable _id_ [0-41] (Function): + name: Ident _id_ [9-12] "Foo" + input: Pat _id_ [12-14]: Unit + output: Type _id_ [17-21]: Path: Path _id_ [17-21] (Ident _id_ [17-21] "Unit") + body: Specializations: + SpecDecl _id_ [24-39] (Body): Gen: Intrinsic, + Stmt _id_ [42-45]: Err + + [ + Error( + Rule( + "expression", + Eof, + Span { + lo: 46, + hi: 46, + }, + ), + ), + ]"#]], + ); +} diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs index 3dfeb10fcd..f63d475777 100644 --- a/compiler/qsc_parse/src/prim.rs +++ b/compiler/qsc_parse/src/prim.rs @@ -4,7 +4,7 @@ #[cfg(test)] mod tests; -use super::{keyword::Keyword, scan::ParserContext, ty::ty, Error, Parser, Result}; +use super::{keyword::Keyword, scan::ParserContext, ty::recovering_ty, Error, Parser, Result}; use crate::{ completion::WordKinds, item::throw_away_doc, @@ -169,7 +169,7 @@ pub(super) fn pat(s: &mut ParserContext) -> Result> { let lo = s.peek().span.lo; let kind = if token(s, TokenKind::Keyword(Keyword::Underscore)).is_ok() { let ty = if token(s, TokenKind::Colon).is_ok() { - Some(Box::new(ty(s)?)) + Some(Box::new(recovering_ty(s)?)) } else { None }; @@ -183,7 +183,7 @@ pub(super) fn pat(s: &mut ParserContext) -> Result> { } else { let name = ident(s).map_err(|e| map_rule_name("pattern", e))?; let ty = if token(s, TokenKind::Colon).is_ok() { - Some(Box::new(ty(s)?)) + Some(Box::new(recovering_ty(s)?)) } else { None }; @@ -252,6 +252,36 @@ where Ok((xs, final_sep)) } +/// Try to parse with the given parser. +/// +/// If the parser fails on the first token, returns the default value. +/// +/// If the parser fails after consuming some tokens, propagates the error. +pub(super) fn parse_or_else( + s: &mut ParserContext, + default: impl FnOnce(Span) -> T, + mut p: impl Parser, +) -> Result { + let lo = s.peek().span.lo; + match p(s) { + Ok(value) => Ok(value), + Err(error) if advanced(s, lo) => Err(error), + Err(error) => { + s.push_error(error); + // The whitespace will become part of the error span + s.skip_trivia(); + Ok(default(s.span(lo))) + } + } +} + +/// Try to parse with the given parser. +/// +/// If the parser fails on the first token, propagates the error. +/// +/// If the parser fails after consuming some tokens, performs +/// recovery by advancing until the next token in `tokens` is found. +/// The recovery token is consumed. pub(super) fn recovering( s: &mut ParserContext, default: impl FnOnce(Span) -> T, @@ -270,6 +300,39 @@ pub(super) fn recovering( } } +/// Try to parse with the given parser. +/// +/// If the parser fails on the first token, returns the default value. +/// +/// If the parser fails after consuming some tokens, performs +/// recovery by advancing until the next token in `tokens` is found. +/// The recovery token is consumed. +/// +/// This behavior is a combination of [`recovering`] and [`parse_or_else`], +/// and provides the most aggressive error recovery. +pub(super) fn recovering_parse_or_else( + s: &mut ParserContext, + default: impl FnOnce(Span) -> T, + tokens: &[TokenKind], + mut p: impl Parser, +) -> T { + let lo = s.peek().span.lo; + match p(s) { + Ok(value) => value, + Err(error) => { + s.push_error(error); + + if advanced(s, lo) { + s.recover(tokens); + } else { + // The whitespace will become part of the error node span + s.skip_trivia(); + } + default(s.span(lo)) + } + } +} + pub(super) fn recovering_semi(s: &mut ParserContext) { if let Err(error) = token(s, TokenKind::Semi) { // no recovery, just move on to the next token diff --git a/compiler/qsc_parse/src/prim/tests.rs b/compiler/qsc_parse/src/prim/tests.rs index 504007f599..b0ba907f8a 100644 --- a/compiler/qsc_parse/src/prim/tests.rs +++ b/compiler/qsc_parse/src/prim/tests.rs @@ -287,17 +287,22 @@ fn pat_missing_ty() { pat, "foo :", &expect![[r#" - Error( - Rule( - "type", - Eof, - Span { - lo: 5, - hi: 5, - }, + Pat _id_ [0-5]: Bind: + Ident _id_ [0-3] "foo" + Type _id_ [5-5]: Err + + [ + Error( + Rule( + "type", + Eof, + Span { + lo: 5, + hi: 5, + }, + ), ), - ) - "#]], + ]"#]], ); } diff --git a/compiler/qsc_parse/src/stmt.rs b/compiler/qsc_parse/src/stmt.rs index 0d5f524f94..30a025444b 100644 --- a/compiler/qsc_parse/src/stmt.rs +++ b/compiler/qsc_parse/src/stmt.rs @@ -15,11 +15,12 @@ use super::{ use crate::{ completion::WordKinds, lex::{Delim, TokenKind}, - prim::{barrier, recovering, recovering_semi, recovering_token}, + prim::{barrier, recovering, recovering_parse_or_else, recovering_semi, recovering_token}, ErrorKind, }; use qsc_ast::ast::{ - Block, Mutability, NodeId, QubitInit, QubitInitKind, QubitSource, Stmt, StmtKind, + Block, Expr, ExprKind, Mutability, NodeId, QubitInit, QubitInitKind, QubitSource, Stmt, + StmtKind, }; use qsc_data_structures::{language_features::LanguageFeatures, span::Span}; @@ -91,17 +92,36 @@ fn parse_local(s: &mut ParserContext) -> Result> { }; let lhs = pat(s)?; - match token(s, TokenKind::Eq) { - Ok(()) => { - let rhs = expr(s)?; - recovering_semi(s); - Ok(Box::new(StmtKind::Local(mutability, lhs, rhs))) + let rhs = match token(s, TokenKind::Eq) { + Ok(()) => + // `Expr` parser with aggressive error recovery. + // If failed at first token, bail immediately and return default. + // If failed and the parser has advanced, recover by scanning until we find a `;` or `}`, + // without consuming it. + { + barrier(s, &[TokenKind::Semi, TokenKind::Close(Delim::Brace)], |s| { + Ok(recovering_parse_or_else( + s, + |span| { + Box::new(Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Err), + }) + }, + &[], + expr, + )) + })? } Err(e) => { s.push_error(e); - Ok(Box::new(StmtKind::Local(mutability, lhs, Box::default()))) + Box::default() } - } + }; + + recovering_semi(s); + Ok(Box::new(StmtKind::Local(mutability, lhs, rhs))) } fn parse_qubit(s: &mut ParserContext) -> Result> { diff --git a/compiler/qsc_parse/src/stmt/tests.rs b/compiler/qsc_parse/src/stmt/tests.rs index a7d8a5127e..60d6eed67f 100644 --- a/compiler/qsc_parse/src/stmt/tests.rs +++ b/compiler/qsc_parse/src/stmt/tests.rs @@ -714,7 +714,10 @@ fn recover_in_block() { "{ let x = 1 +; x }", &expect![[r#" Block _id_ [0-18]: - Stmt _id_ [2-14]: Err + Stmt _id_ [2-14]: Local (Immutable): + Pat _id_ [6-7]: Bind: + Ident _id_ [6-7] "x" + Expr _id_ [10-13]: Err Stmt _id_ [15-16]: Expr: Expr _id_ [15-16]: Path: Path _id_ [15-16] (Ident _id_ [15-16] "x") [ @@ -781,7 +784,10 @@ fn recover_statements_before_and_after() { Expr _id_ [22-27]: BinOp (Add): Expr _id_ [22-23]: Lit: Int(2) Expr _id_ [26-27]: Lit: Int(2) - Stmt _id_ [41-81]: Err + Stmt _id_ [41-81]: Local (Immutable): + Pat _id_ [45-46]: Bind: + Ident _id_ [45-46] "y" + Expr _id_ [49-80]: Err Stmt _id_ [94-95]: Expr: Expr _id_ [94-95]: Path: Path _id_ [94-95] (Ident _id_ [94-95] "z") [ diff --git a/compiler/qsc_parse/src/ty.rs b/compiler/qsc_parse/src/ty.rs index e5c82c7d3e..cdec62f051 100644 --- a/compiler/qsc_parse/src/ty.rs +++ b/compiler/qsc_parse/src/ty.rs @@ -14,7 +14,7 @@ use crate::{ completion::WordKinds, item::throw_away_doc, lex::{ClosedBinOp, Delim, TokenKind}, - prim::recovering_path, + prim::{parse_or_else, recovering_path}, ErrorKind, }; use qsc_ast::ast::{ @@ -28,6 +28,18 @@ pub(super) fn ty(s: &mut ParserContext) -> Result { array_or_arrow(s, lhs, lo) } +pub(super) fn recovering_ty(s: &mut ParserContext) -> Result { + parse_or_else( + s, + |span| Ty { + id: NodeId::default(), + span, + kind: Box::new(TyKind::Err), + }, + ty, + ) +} + pub(super) fn array_or_arrow(s: &mut ParserContext<'_>, mut lhs: Ty, lo: u32) -> Result { loop { if let Some(()) = opt(s, array)? { @@ -37,7 +49,7 @@ pub(super) fn array_or_arrow(s: &mut ParserContext<'_>, mut lhs: Ty, lo: u32) -> kind: Box::new(TyKind::Array(Box::new(lhs))), } } else if let Some(kind) = opt(s, arrow)? { - let output = ty(s)?; + let output = recovering_ty(s)?; let functors = if token(s, TokenKind::Keyword(Keyword::Is)).is_ok() { Some(Box::new(functor_expr(s)?)) } else { diff --git a/language_service/src/completion/tests.rs b/language_service/src/completion/tests.rs index ba9195044c..9db10736d3 100644 --- a/language_service/src/completion/tests.rs +++ b/language_service/src/completion/tests.rs @@ -1472,6 +1472,67 @@ fn notebook_auto_open_start_of_cell() { ); } +#[test] +fn notebook_last_expr() { + check_notebook( + &[( + "cell1", + indoc! {" + //qsharp + function Foo() : Unit {} + 3 + ↘" + }, + )], + &["Foo", "Fake"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Foo", + kind: Function, + sort_text: Some( + "0100Foo", + ), + detail: Some( + "function Foo() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Fake", + kind: Function, + sort_text: Some( + "0401Fake", + ), + detail: Some( + "operation Fake() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Fake;\n", + range: Range { + start: Position { + line: 1, + column: 0, + }, + end: Position { + line: 1, + column: 0, + }, + }, + }, + ], + ), + }, + ), + ] + "#]], + ); +} + #[test] fn local_vars() { check( @@ -3804,3 +3865,210 @@ fn field_access_local_shadows_global() { "#]], ); } + +#[test] +fn ty_param_in_signature() { + check( + r"namespace Test { + operation Test<'T>(x: ↘) : Unit {} + }", + &["'T", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "'T", + kind: TypeParameter, + sort_text: Some( + "0100'T", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0600FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn ty_param_in_return_type() { + check( + r"namespace Test { + operation Test<'T>(x: 'T) : ↘ {} + }", + &["'T", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "'T", + kind: TypeParameter, + sort_text: Some( + "0100'T", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0600FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn path_segment_in_return_type() { + check( + r"namespace Test { + operation Test(x: 'T) : FakeStdLib.↘ {} + }", + &["Udt"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn return_type_in_partial_callable_signature() { + check( + r"namespace Test { + operation Test<'T>() : ↘ + }", + &["'T", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "'T", + kind: TypeParameter, + sort_text: Some( + "0100'T", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0600FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn arg_type_in_partial_callable_signature() { + check( + r"namespace Test { + operation Test<'T>(x: ↘) + }", + &["'T", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "'T", + kind: TypeParameter, + sort_text: Some( + "0100'T", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0600FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn incomplete_return_type_in_partial_callable_signature() { + check( + r"namespace Test { + operation Test<'T>() : () => ↘ + }", + &["'T", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "'T", + kind: TypeParameter, + sort_text: Some( + "0100'T", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0600FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +}