Skip to content

Commit 4dcb22f

Browse files
committed
refactor(diagnostics): unify number-literal detection helpers
Replace the direct `robot.variables.finders.NumberFinder` dependency and the two duplicated `_try_resolve_number_literal` static methods with shared `try_resolve_number_literal` / `is_number_literal` helpers in `robot.utils.variables`, consumed from `model_helper`, `namespace_analyzer` and the semantic `analyzer`.
1 parent bc488d0 commit 4dcb22f

4 files changed

Lines changed: 54 additions & 85 deletions

File tree

packages/robot/src/robotcode/robot/diagnostics/model_helper.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from robot.errors import VariableError
2222
from robot.parsing.lexer.tokens import Token
2323
from robot.utils.escaping import unescape
24-
from robot.variables.finders import NOT_FOUND, NumberFinder
2524
from robotcode.core.lsp.types import Position
2625

2726
from ..utils import RF_VERSION
@@ -33,7 +32,7 @@
3332
whitespace_at_begin_of_token,
3433
whitespace_from_begin_of_token,
3534
)
36-
from ..utils.variables import contains_variable, search_variable, split_from_equals
35+
from ..utils.variables import contains_variable, is_number_literal, search_variable, split_from_equals
3736
from .entities import (
3837
LibraryEntry,
3938
VariableDefinition,
@@ -395,12 +394,6 @@ def iter_variables_from_token(
395394
skip_local_variables: bool = False,
396395
return_not_found: bool = False,
397396
) -> Iterator[Tuple[Token, VariableDefinition]]:
398-
def is_number(name: str) -> bool:
399-
if name.startswith("$"):
400-
finder = NumberFinder()
401-
return bool(finder.find(name) != NOT_FOUND)
402-
return False
403-
404397
def iter_token(
405398
to: Token, ignore_errors: bool = False
406399
) -> Iterator[Union[Token, Tuple[Token, VariableDefinition]]]:
@@ -483,7 +476,7 @@ def iter_token(
483476
yield strip_variable_token(sub_token), var
484477
continue
485478

486-
if is_number(sub_token.value):
479+
if is_number_literal(sub_token.value):
487480
continue
488481

489482
if (
@@ -511,7 +504,7 @@ def iter_token(
511504
if var is not None:
512505
yield strip_variable_token(sub_sub_token), var
513506
continue
514-
if is_number(name):
507+
if is_number_literal(name):
515508
continue
516509
elif return_not_found:
517510
if contains_variable(sub_token.value[2:-1]):

packages/robot/src/robotcode/robot/diagnostics/namespace_analyzer.py

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
VariablesImport,
2929
)
3030
from robot.utils.escaping import unescape
31-
from robot.variables.finders import NOT_FOUND, NumberFinder
3231
from robotcode.core.concurrent import check_current_task_canceled
3332
from robotcode.core.lsp.types import (
3433
CodeDescription,
@@ -61,9 +60,11 @@
6160
InvalidVariableError,
6261
VariableMatcher,
6362
contains_variable,
63+
is_number_literal,
6464
replace_curdir_in_variable_values,
6565
search_variable,
6666
split_from_equals,
67+
try_resolve_number_literal,
6768
)
6869
from ..utils.visitor import Visitor
6970
from .analyzer_result import AnalyzerResult
@@ -1919,12 +1920,6 @@ def _find_variable(self, name: str) -> Optional[VariableDefinition]:
19191920
except (VariableError, InvalidVariableError):
19201921
return None
19211922

1922-
def _is_number(self, name: str) -> bool:
1923-
if name.startswith("$"):
1924-
finder = NumberFinder()
1925-
return bool(finder.find(name) != NOT_FOUND)
1926-
return False
1927-
19281923
def _try_resolve_nested_variable_base(
19291924
self, identifier: str, base: str, name_token: Token
19301925
) -> Union[str, Literal[False], tuple[None, str]]:
@@ -2004,7 +1999,7 @@ def _resolve_variable_to_string(self, var_ref: str, depth: int = 0) -> Union[str
20041999
var_def = self._find_variable(var_ref)
20052000
if var_def is None:
20062001
# RF's NumberFinder: ${1}, ${3.14}, ${0xFF}, ${0b1010}, ${0o17}
2007-
number_str = self._try_resolve_number_literal(var_ref)
2002+
number_str = try_resolve_number_literal(var_ref)
20082003
if number_str is not None:
20092004
return number_str
20102005
# RF's ExtendedFinder: ${VAR.attr}, ${VAR[key]}, ${1-2}, etc.
@@ -2030,33 +2025,6 @@ def _resolve_variable_to_string(self, var_ref: str, depth: int = 0) -> Union[str
20302025

20312026
return " ".join(resolved_items)
20322027

2033-
@staticmethod
2034-
def _try_resolve_number_literal(var_ref: str) -> Optional[str]:
2035-
"""Detect RF number literals like ``${1}``, ``${3.14}``, ``${0xFF}``.
2036-
2037-
Mimics RF's ``NumberFinder``: strips spaces, lowercases, then tries
2038-
``int()`` (with ``0b``/``0o``/``0x`` prefix support) and ``float()``.
2039-
Returns the string representation of the number, or ``None``.
2040-
"""
2041-
inner = "".join(var_ref[2:-1].split()).casefold()
2042-
if not inner:
2043-
return None
2044-
bases = {"0b": 2, "0o": 8, "0x": 16}
2045-
for prefix, base in bases.items():
2046-
if inner.startswith(prefix):
2047-
try:
2048-
return str(int(inner[2:], base))
2049-
except ValueError:
2050-
return None
2051-
try:
2052-
return str(int(inner))
2053-
except ValueError:
2054-
pass
2055-
try:
2056-
return str(float(inner))
2057-
except ValueError:
2058-
return None
2059-
20602028
def _is_extended_with_known_base(self, var_ref: str) -> bool:
20612029
"""Check if ``var_ref`` matches RF's extended variable syntax with a resolvable base.
20622030
@@ -2073,7 +2041,7 @@ def _is_extended_with_known_base(self, var_ref: str) -> bool:
20732041
base_ref = f"${{{base_name}}}"
20742042
if self._find_variable(base_ref) is not None:
20752043
return True
2076-
if self._try_resolve_number_literal(base_ref) is not None:
2044+
if try_resolve_number_literal(base_ref) is not None:
20772045
return True
20782046
return False
20792047

@@ -2276,7 +2244,7 @@ def _iter_variables_from_token(self, token: Token) -> Iterator[Tuple[Token, Vari
22762244
)
22772245
continue
22782246

2279-
if self._is_number(var_token.value):
2247+
if is_number_literal(var_token.value):
22802248
continue
22812249

22822250
if (
@@ -2299,7 +2267,7 @@ def _iter_variables_from_token(self, token: Token) -> Iterator[Tuple[Token, Vari
22992267
if var is not None:
23002268
yield strip_variable_token(sub_sub_token), var
23012269
continue
2302-
if self._is_number(name):
2270+
if is_number_literal(name):
23032271
continue
23042272
else:
23052273
if contains_variable(var_token.value[2:-1], "$@&%"):

packages/robot/src/robotcode/robot/diagnostics/semantic_analyzer/analyzer.py

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
from robot.parsing.model.statements import Error as RfError
6565
from robot.parsing.model.statements import ReturnStatement as RfReturnStatement
6666
from robot.utils.escaping import unescape
67-
from robot.variables.finders import NOT_FOUND, NumberFinder
6867
from robotcode.core.concurrent import check_current_task_canceled
6968
from robotcode.core.lsp.types import (
7069
CodeDescription,
@@ -97,9 +96,11 @@
9796
InvalidVariableError,
9897
VariableMatcher,
9998
contains_variable,
99+
is_number_literal,
100100
replace_curdir_in_variable_values,
101101
search_variable,
102102
split_from_equals,
103+
try_resolve_number_literal,
103104
)
104105
from ...utils.visitor import Visitor
105106
from ..analyzer_result import AnalyzerResult
@@ -3757,12 +3758,6 @@ def _find_variable(self, name: str) -> Optional[VariableDefinition]:
37573758
except (VariableError, InvalidVariableError):
37583759
return None
37593760

3760-
def _is_number(self, name: str) -> bool:
3761-
if name.startswith("$"):
3762-
finder = NumberFinder()
3763-
return bool(finder.find(name) != NOT_FOUND)
3764-
return False
3765-
37663761
def _try_resolve_nested_variable_base(
37673762
self, identifier: str, base: str, name_token: Token
37683763
) -> Union[str, Literal[False], tuple[None, str]]:
@@ -3842,7 +3837,7 @@ def _resolve_variable_to_string(self, var_ref: str, depth: int = 0) -> Union[str
38423837
var_def = self._find_variable(var_ref)
38433838
if var_def is None:
38443839
# RF's NumberFinder: ${1}, ${3.14}, ${0xFF}, ${0b1010}, ${0o17}
3845-
number_str = self._try_resolve_number_literal(var_ref)
3840+
number_str = try_resolve_number_literal(var_ref)
38463841
if number_str is not None:
38473842
return number_str
38483843
# RF's ExtendedFinder: ${VAR.attr}, ${VAR[key]}, ${1-2}, etc.
@@ -3868,33 +3863,6 @@ def _resolve_variable_to_string(self, var_ref: str, depth: int = 0) -> Union[str
38683863

38693864
return " ".join(resolved_items)
38703865

3871-
@staticmethod
3872-
def _try_resolve_number_literal(var_ref: str) -> Optional[str]:
3873-
"""Detect RF number literals like ``${1}``, ``${3.14}``, ``${0xFF}``.
3874-
3875-
Mimics RF's ``NumberFinder``: strips spaces, lowercases, then tries
3876-
``int()`` (with ``0b``/``0o``/``0x`` prefix support) and ``float()``.
3877-
Returns the string representation of the number, or ``None``.
3878-
"""
3879-
inner = "".join(var_ref[2:-1].split()).casefold()
3880-
if not inner:
3881-
return None
3882-
bases = {"0b": 2, "0o": 8, "0x": 16}
3883-
for prefix, base in bases.items():
3884-
if inner.startswith(prefix):
3885-
try:
3886-
return str(int(inner[2:], base))
3887-
except ValueError:
3888-
return None
3889-
try:
3890-
return str(int(inner))
3891-
except ValueError:
3892-
pass
3893-
try:
3894-
return str(float(inner))
3895-
except ValueError:
3896-
return None
3897-
38983866
def _is_extended_with_known_base(self, var_ref: str) -> bool:
38993867
"""Check if ``var_ref`` matches RF's extended variable syntax with a resolvable base.
39003868
@@ -3911,7 +3879,7 @@ def _is_extended_with_known_base(self, var_ref: str) -> bool:
39113879
base_ref = f"${{{base_name}}}"
39123880
if self._find_variable(base_ref) is not None:
39133881
return True
3914-
if self._try_resolve_number_literal(base_ref) is not None:
3882+
if try_resolve_number_literal(base_ref) is not None:
39153883
return True
39163884
return False
39173885

@@ -4106,7 +4074,7 @@ def _resolve_variable_occurrence(
41064074
yield (reference_token, var)
41074075
return
41084076

4109-
if self._is_number(occurrence.lookup_name):
4077+
if is_number_literal(occurrence.lookup_name):
41104078
return
41114079

41124080
if occurrence.strip_for_reference and occurrence.value.startswith(("${", "@{", "&{", "%{")):

packages/robot/src/robotcode/robot/utils/variables.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,46 @@ def split_from_equals(string: str) -> Tuple[str, Optional[str]]:
228228
return cast(Tuple[str, Optional[str]], robot_split_from_equals(string))
229229

230230

231+
_NUMBER_LITERAL_BASES = {"0b": 2, "0o": 8, "0x": 16}
232+
233+
234+
@functools.lru_cache(maxsize=1024)
235+
def try_resolve_number_literal(var_ref: str) -> Optional[str]:
236+
"""Detect RF number literals like ``${1}``, ``${3.14}``, ``${0xFF}``.
237+
238+
Static reimplementation of RF's ``NumberFinder`` (which we avoid depending on
239+
directly for compatibility): strips spaces, lowercases, then tries ``int()``
240+
with ``0b`` / ``0o`` / ``0x`` prefix support, falling back to ``float()``.
241+
Only ``${...}`` form is accepted; anything else returns ``None``.
242+
243+
Returns the canonical string representation of the number, or ``None``.
244+
"""
245+
if not (var_ref.startswith("${") and var_ref.endswith("}")):
246+
return None
247+
inner = "".join(var_ref[2:-1].split()).casefold()
248+
if not inner:
249+
return None
250+
for prefix, base in _NUMBER_LITERAL_BASES.items():
251+
if inner.startswith(prefix):
252+
try:
253+
return str(int(inner[2:], base))
254+
except ValueError:
255+
return None
256+
try:
257+
return str(int(inner))
258+
except ValueError:
259+
pass
260+
try:
261+
return str(float(inner))
262+
except ValueError:
263+
return None
264+
265+
266+
def is_number_literal(name: str) -> bool:
267+
"""Check if ``name`` is an RF scalar number literal like ``${1}``, ``${3.14}``."""
268+
return try_resolve_number_literal(name) is not None
269+
270+
231271
def replace_curdir_in_variable_values(values: Sequence[str], source: str) -> Tuple[str, ...]:
232272
"""Replace ${CURDIR} in variable values with the escaped parent directory of source."""
233273
curdir = str(Path(source).parent).replace("\\", "\\\\")

0 commit comments

Comments
 (0)