diff --git a/tools/chpl-language-server/src/chpl-language-server.py b/tools/chpl-language-server/src/chpl-language-server.py index aba5c43f14d3..6cdea80286eb 100755 --- a/tools/chpl-language-server/src/chpl-language-server.py +++ b/tools/chpl-language-server/src/chpl-language-server.py @@ -142,6 +142,59 @@ import argparse import configargparse +REAL_NUMBERIC = (chapel.RealLiteral, chapel.IntLiteral, chapel.UintLiteral) +NUMERIC = REAL_NUMBERIC + (chapel.ImagLiteral,) + + +def is_basic_literal_like(node: chapel.AstNode) -> Optional[chapel.Literal]: + """ + Check for "basic" literals: basically, 1, "hello", -42, etc. + Returns the "underlying" literal removing surrounding AST (1, "hello", 42). + This helps do type comparisons in more complex checks. If the node is + not a basic literal, returns None. + """ + if isinstance(node, chapel.Literal): + return node + + if ( + isinstance(node, chapel.OpCall) + and node.op() == "-" + and node.num_actuals() == 1 + ): + # Do not recurse; do not consider --42 as a basic literal. + act = node.actual(0) + if isinstance(act, NUMERIC): + return act + + return None + + +def is_literal_like(node: chapel.AstNode) -> bool: + if is_basic_literal_like(node): + return True + + if isinstance(node, chapel.OpCall): + # A complex number is far from a literal in the AST; in fact, it + # potentially has as many as 4 AST nodes: -1 + 2i has a unary negation, + # an addition, and two "pure" literals. + op = node.op() + if (op == "+" or op == "-") and node.num_actuals() == 2: + # The left element can be a 'basic literal-like', like 1 or -42i. + # But the right element shouldn't have any operators, otherwise + # we'd get somethig that looks like 1 - -42i. So we just + # use the right argument directly. + left = is_basic_literal_like(node.actual(0)) + right = node.actual(1) + + real_number = REAL_NUMBERIC + imag_number = chapel.ImagLiteral + if isinstance(left, real_number) and isinstance(right, imag_number): + return True + if isinstance(left, imag_number) and isinstance(right, real_number): + return True + + return False + def decl_kind(decl: chapel.NamedDecl) -> Optional[SymbolKind]: if isinstance(decl, chapel.Module) and decl.kind() != "implicit": @@ -1222,7 +1275,7 @@ def get_call_inlays( # tuples. We don't need hints for those. continue - if not isinstance(act, chapel.core.Literal): + if not is_literal_like(act): # Only show named arguments for literals. continue diff --git a/tools/chpl-language-server/test/call_inlays.py b/tools/chpl-language-server/test/call_inlays.py index 26e0f1924681..96319914bda9 100644 --- a/tools/chpl-language-server/test/call_inlays.py +++ b/tools/chpl-language-server/test/call_inlays.py @@ -75,7 +75,6 @@ async def test_call_inlays(client: LanguageClient): @pytest.mark.asyncio -@pytest.mark.xfail async def test_call_inlays_complex(client: LanguageClient): """ Ensure that call inlays are shown for complex literals in call expressions. @@ -84,9 +83,44 @@ async def test_call_inlays_complex(client: LanguageClient): file = """ proc foo(a) {} foo(3i+10); + foo(3i+10.0); + foo(3.0i+10.0); + foo(3.0i+10); + foo(10+3i); + foo(10.0+3i); + foo(10.0+3.0i); + foo(10+3.0i); + + // Not 'literals' in our sense: + foo(3i + 4i); + foo(1 + 1); + foo((-1) - (-i)); """ - inlays = [(pos((1, 4)), "a = ", None)] + inlays = [(pos((i, 4)), "a = ", None) for i in range(1, 9)] + + async with source_file(client, file) as doc: + await check_inlay_hints(client, doc, rng((0, 0), endpos(file)), inlays) + + +@pytest.mark.asyncio +async def test_call_inlays_negative(client: LanguageClient): + """ + Ensure that call inlays are shown for negative literals in call expressions. + """ + + file = """ + proc foo(a) {} + foo(-42); + foo(-42.0); + foo(-42i); + foo(-42.0i); + + // Not 'literals' in our sense: + foo(- -42); + """ + + inlays = [(pos((i, 4)), "a = ", None) for i in range(1, 5)] async with source_file(client, file) as doc: await check_inlay_hints(client, doc, rng((0, 0), endpos(file)), inlays)