From ee5886555e44268759ec3f7d99f45f316a50caad Mon Sep 17 00:00:00 2001 From: Neil Brown Date: Fri, 24 Jul 2026 16:43:08 +0200 Subject: [PATCH 1/2] Fix member completion collapsing to builtin dump for parenthesized literals Two independent bugs in dot-completion, both surfacing as a fallback to listing every builtin name instead of the actual type's members: - Completer.getNameFilter used an inclusive token-range bound, so a `)` closing an enclosing call immediately after the caret (e.g. `wrap((expr).)`) got swept into the range and broke the length/last-token checks that recognize `(expr).attr`. - AstNode.Value/BooleanValue (int/float/complex/bool/None literals) never tracked an end position, unlike StringValue, so they couldn't be registered by position for completion, and Scope.findName had no case for them either. This broke `(5).attr`-style completion even at top level, unrelated to the call-wrapping bug above. --- .../scala/tigerpython/parser/ast/AstNode.scala | 14 ++++++++++++-- .../parser/parsing/ExpressionParser.scala | 16 +++++++++++++--- .../utilities/completer/Completer.scala | 7 ++++++- .../tigerpython/utilities/scopes/Scope.scala | 6 ++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/tpParser/shared/src/main/scala/tigerpython/parser/ast/AstNode.scala b/tpParser/shared/src/main/scala/tigerpython/parser/ast/AstNode.scala index 474fd1a..1a03f49 100755 --- a/tpParser/shared/src/main/scala/tigerpython/parser/ast/AstNode.scala +++ b/tpParser/shared/src/main/scala/tigerpython/parser/ast/AstNode.scala @@ -459,7 +459,9 @@ object AstNode { override def isValidAssignTarget: Boolean = true override def toString: String = "(%s)".format(names.mkString(", ")) } - case class BooleanValue(pos: Int, value: Boolean) extends Expression(AstNodeKind.CONSTANT) { + case class BooleanValue(pos: Int, value: Boolean) extends Expression(AstNodeKind.CONSTANT) with Span { + // `True`/`False` are fixed-width keywords, so the end position needs no extra state to track. + def endPos: Int = pos + (if (value) 4 else 5) def notToString: String = if (value) "False" else "True" override def toString: String = if (value) "True" else "False" } @@ -471,26 +473,34 @@ object AstNode { def apply(token: Token): Value = { val result = new Value(token.pos, ValueType.fromTokenType(token.tokenType)) result.value = token.value + result.endPos = token.endPos result } def apply(pos: Int, intValue: Int): Value = { val result = new Value(pos, ValueType.INTEGER) result.value = intValue.toString + result.endPos = pos + result.value.length result } } - case class Value(pos: Int, valueType: ValueType.Value) extends Expression(AstNodeKind.CONSTANT) { + case class Value(pos: Int, valueType: ValueType.Value) extends Expression(AstNodeKind.CONSTANT) with Span { var value: String = _ + // Defaults to a zero-width span at `pos` for values not constructed from a real source token + // (e.g. synthetic placeholders inserted during error recovery); real construction sites set + // this from the originating token's `endPos`. + var endPos: Int = pos def createNegative(): Value = if (value != null && value != "" && (valueType == ValueType.INTEGER || valueType == ValueType.FLOAT)) { if (value(0) == '-') { val result = Value(pos + 1, valueType) result.value = value.drop(1) + result.endPos = result.pos + result.value.length result } else { val result = Value(pos - 1, valueType) result.value = "-" + value + result.endPos = result.pos + result.value.length result } } else diff --git a/tpParser/shared/src/main/scala/tigerpython/parser/parsing/ExpressionParser.scala b/tpParser/shared/src/main/scala/tigerpython/parser/parsing/ExpressionParser.scala index f888e36..4c914f9 100755 --- a/tpParser/shared/src/main/scala/tigerpython/parser/parsing/ExpressionParser.scala +++ b/tpParser/shared/src/main/scala/tigerpython/parser/parsing/ExpressionParser.scala @@ -648,16 +648,24 @@ class ExpressionParser(val parser: Parser, val parserState: ParserState) { checkMissingOperator(tokens) val result = AstNode.Value(token.pos, ValueType.FLOAT) result.value = token.value + result.endPos = token.endPos result case TokenType.INT | TokenType.LONG => checkMissingOperator(tokens) val result = AstNode.Value(token.pos, ValueType.INTEGER) result.value = token.value + result.endPos = token.endPos result case TokenType.COMPLEX => - AstNode.Value(token.pos, ValueType.COMPLEX) + val result = AstNode.Value(token.pos, ValueType.COMPLEX) + result.value = token.value + result.endPos = token.endPos + result case TokenType.NONE => - AstNode.Value(token.pos, ValueType.NONE) + val result = AstNode.Value(token.pos, ValueType.NONE) + result.value = token.value + result.endPos = token.endPos + result case TokenType.TRUE => AstNode.BooleanValue(token.pos, value = true) case TokenType.FALSE => @@ -679,7 +687,9 @@ class ExpressionParser(val parser: Parser, val parserState: ParserState) { case TokenType.BYTEARRAY => while (tokens.hasType(TokenType.BYTEARRAY)) tokens.next() - AstNode.Value(token.pos, ValueType.BYTE_ARRAY) + val result = AstNode.Value(token.pos, ValueType.BYTE_ARRAY) + result.endPos = tokens.prevEndPos + result case TokenType.LEFT_PARENS => // Check for Lisp-Syntax if (tokens.getIndex <= 1 && tokens.peekType(1) == TokenType.NAME) diff --git a/tpParser/shared/src/main/scala/tigerpython/utilities/completer/Completer.scala b/tpParser/shared/src/main/scala/tigerpython/utilities/completer/Completer.scala index c3b9985..d2fd2da 100644 --- a/tpParser/shared/src/main/scala/tigerpython/utilities/completer/Completer.scala +++ b/tpParser/shared/src/main/scala/tigerpython/utilities/completer/Completer.scala @@ -116,7 +116,12 @@ class Completer(val moduleName: String, } nameWalker.getNodeForPosition(caretPos) match { case Some(prefixName) => - val tokenRange = tokenLine.getTokenRange(prefixName.endPos, caretPos) + // `getTokenRange` includes tokens whose `pos` equals `caretPos` (its upper bound is + // inclusive), but a token starting exactly at the caret lies after it, not within the + // range up to it -- e.g. the `)` closing an enclosing call immediately after the caret + // in `wrap((expr).)`. Left in, that stray token breaks the length/last-token + // checks below, so it's filtered out here. + val tokenRange = tokenLine.getTokenRange(prefixName.endPos, caretPos).filter(_.pos < caretPos) if (0 < tokenRange.length && tokenRange.length <= 2 && tokenRange(0).tokenType == TokenType.DOT) { val n = if (filterType == FilterType.IMPORT_FROM) scope.findName(moduleBase, prefixName) diff --git a/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/Scope.scala b/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/Scope.scala index 363129b..55e7cbc 100644 --- a/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/Scope.scala +++ b/tpParser/shared/src/main/scala/tigerpython/utilities/scopes/Scope.scala @@ -148,6 +148,12 @@ abstract class Scope { Some(types.BuiltinTypes.LIST) case _: AstNode.StringValue => Some(types.BuiltinTypes.STRING) + case expr: AstNode.Expression => + // Catch-all for literal/expression kinds with no bespoke case above (int/float/complex/none + // literals, booleans, unary/binary ops, comparisons, ...): TypeAstWalker.getType already + // knows how to resolve these, so delegate rather than falling through to None, which would + // make e.g. `(5).bit_length` unresolvable and fall back to a full builtin-name dump. + Some(typeAstWalker.getType(expr)) case _ => None } From 31f7b4e0ca619b72c6bcadeda1c0a2147c6bd444 Mon Sep 17 00:00:00 2001 From: Neil Brown Date: Fri, 24 Jul 2026 16:51:26 +0200 Subject: [PATCH 2/2] Add regression tests for the two parenthesized-literal completion bugs - completer_paren_wrapped_call_var/string_literal/method_result: cover the enclosing-call token-range bug (wrap((expr).)). - completer_int_literal_unwrapped: covers the missing-endPos bug on its own, at top level with no enclosing call. - completer_paren_wrapped_call_int_literal: end-to-end case combining both fixes, matching the original bug report. Verified each new test fails against the pre-fix code and passes with it. --- .../programs/completer/completer_int_literal_unwrapped.py | 3 +++ .../completer/completer_paren_wrapped_call_int_literal.py | 3 +++ .../completer/completer_paren_wrapped_call_method_result.py | 3 +++ .../completer/completer_paren_wrapped_call_string_literal.py | 3 +++ .../programs/completer/completer_paren_wrapped_call_var.py | 4 ++++ 5 files changed, 16 insertions(+) create mode 100644 tpParser/shared/src/test/programs/completer/completer_int_literal_unwrapped.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_int_literal.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_method_result.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_string_literal.py create mode 100644 tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_var.py diff --git a/tpParser/shared/src/test/programs/completer/completer_int_literal_unwrapped.py b/tpParser/shared/src/test/programs/completer/completer_int_literal_unwrapped.py new file mode 100644 index 0000000..b91bc40 --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_int_literal_unwrapped.py @@ -0,0 +1,3 @@ +# 4 +# bit_length +(5). diff --git a/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_int_literal.py b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_int_literal.py new file mode 100644 index 0000000..a8cca92 --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_int_literal.py @@ -0,0 +1,3 @@ +# 9 +# bit_length +wrap((5).) diff --git a/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_method_result.py b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_method_result.py new file mode 100644 index 0000000..280912d --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_method_result.py @@ -0,0 +1,3 @@ +# 19 +# capitalize;center;count;decode;encode;endswith;expandtabs;find;format;index;isalnum;isalpha;isdigit;islower;isspace;istitle;isupper;join;ljust;lower;lstrip;partition;replace;rfind;rindex;rjust;rpartition;rsplit;rstrip;split;splitlines;startswith;strip;swapcase;title;translate;upper;zfill +wrap(("a".upper()).) diff --git a/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_string_literal.py b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_string_literal.py new file mode 100644 index 0000000..0ed2c7b --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_string_literal.py @@ -0,0 +1,3 @@ +# 11 +# capitalize;center;count;decode;encode;endswith;expandtabs;find;format;index;isalnum;isalpha;isdigit;islower;isspace;istitle;isupper;join;ljust;lower;lstrip;partition;replace;rfind;rindex;rjust;rpartition;rsplit;rstrip;split;splitlines;startswith;strip;swapcase;title;translate;upper;zfill +wrap(("a").) diff --git a/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_var.py b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_var.py new file mode 100644 index 0000000..e872094 --- /dev/null +++ b/tpParser/shared/src/test/programs/completer/completer_paren_wrapped_call_var.py @@ -0,0 +1,4 @@ +# 47 +# capitalize;center;count;decode;encode;endswith;expandtabs;find;format;index;isalnum;isalpha;isdigit;islower;isspace;istitle;isupper;join;ljust;lower;lstrip;partition;replace;rfind;rindex;rjust;rpartition;rsplit;rstrip;split;splitlines;startswith;strip;swapcase;title;translate;upper;zfill +myString = "Hello from Strype" +wrap((myString).)