Skip to content

Commit 78a26c3

Browse files
committed
perf(robot): patch RF's variable_not_found to skip slow RecommendationFinder
Robot Framework's variable_not_found() calls RecommendationFinder for fuzzy "Did you mean...?" suggestions on every unresolved variable. This is O(n*m) over all variable candidates and extremely slow for large projects (see #587: 30+ min for 375 files). RobotCode never uses the recommendation text — all VariableError catch sites either ignore the error or use it for unrelated diagnostics. The monkey-patch replaces variable_not_found in all 5 RF modules that import it (notfound, finders, evaluation, store, __init__) with a fast version that raises VariableError with just the base message.
1 parent 7e74c2d commit 78a26c3

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
)
7878
from ..utils.markdownformatter import MarkDownFormatter
7979
from ..utils.match import normalize, normalize_namespace
80+
from ..utils.robot_patching import patch_variable_not_found
8081
from ..utils.variables import contains_variable, search_variable
8182
from .entities import (
8283
ArgumentDefinition,
@@ -114,6 +115,7 @@
114115
from robot.running.resourcemodel import ResourceFile
115116
from robot.utils import NOT_SET as robot_notset # type: ignore[no-redef] # noqa: N811
116117

118+
patch_variable_not_found()
117119

118120
RUN_KEYWORD_NAMES = [
119121
"Run Keyword",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import sys
2+
from types import ModuleType
3+
from typing import Optional
4+
5+
from robot.errors import VariableError
6+
7+
8+
def _fast_variable_not_found(
9+
name: str,
10+
candidates: object,
11+
message: Optional[str] = None,
12+
deco_braces: bool = True,
13+
) -> None:
14+
raise VariableError(message or f"Variable '{name}' not found.")
15+
16+
17+
_PATCHED = False
18+
19+
20+
def patch_variable_not_found() -> None:
21+
"""Replace Robot Framework's variable_not_found with a fast version.
22+
23+
The original uses RecommendationFinder for fuzzy "Did you mean...?"
24+
suggestions, which is O(n*m) and extremely slow for large projects.
25+
RobotCode never uses the recommendation text, so we skip it.
26+
27+
Must be called after robot.variables is imported.
28+
"""
29+
global _PATCHED
30+
31+
if _PATCHED:
32+
return
33+
34+
_PATCHED = True
35+
36+
modules = [
37+
"robot.variables.notfound",
38+
"robot.variables.finders",
39+
"robot.variables.evaluation",
40+
"robot.variables.store",
41+
"robot.variables",
42+
]
43+
for mod_name in modules:
44+
mod: Optional[ModuleType] = sys.modules.get(mod_name)
45+
if mod is not None and hasattr(mod, "variable_not_found"):
46+
mod.variable_not_found = _fast_variable_not_found # type: ignore[attr-defined]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import sys
5+
6+
import pytest
7+
from robot.errors import VariableError
8+
9+
from robotcode.robot.utils.robot_patching import (
10+
_fast_variable_not_found,
11+
patch_variable_not_found,
12+
)
13+
14+
PATCHED_MODULES = [
15+
"robot.variables.notfound",
16+
"robot.variables.finders",
17+
"robot.variables.evaluation",
18+
"robot.variables.store",
19+
"robot.variables",
20+
]
21+
22+
23+
@pytest.fixture(autouse=True)
24+
def _reset_patch_state() -> None:
25+
import robotcode.robot.utils.robot_patching as mod
26+
27+
mod._PATCHED = False
28+
29+
30+
def _ensure_modules_loaded() -> None:
31+
for name in PATCHED_MODULES:
32+
if name not in sys.modules:
33+
importlib.import_module(name)
34+
35+
36+
def test_patch_replaces_variable_not_found_in_all_modules() -> None:
37+
_ensure_modules_loaded()
38+
patch_variable_not_found()
39+
40+
for mod_name in PATCHED_MODULES:
41+
mod = sys.modules[mod_name]
42+
assert hasattr(mod, "variable_not_found"), f"{mod_name} missing variable_not_found"
43+
assert mod.variable_not_found is _fast_variable_not_found, f"{mod_name}.variable_not_found was not patched"
44+
45+
46+
def test_patch_is_idempotent() -> None:
47+
_ensure_modules_loaded()
48+
patch_variable_not_found()
49+
patch_variable_not_found()
50+
51+
for mod_name in PATCHED_MODULES:
52+
mod = sys.modules[mod_name]
53+
assert mod.variable_not_found is _fast_variable_not_found
54+
55+
56+
def test_fast_variable_not_found_raises_without_recommendation() -> None:
57+
candidates = {"foo": 1, "bar": 2, "baz": 3}
58+
with pytest.raises(VariableError, match=r"Variable '\$\{foobar\}' not found\.$"):
59+
_fast_variable_not_found("${foobar}", candidates)
60+
61+
62+
def test_fast_variable_not_found_uses_custom_message() -> None:
63+
with pytest.raises(VariableError, match="custom error"):
64+
_fast_variable_not_found("${x}", {}, message="custom error")
65+
66+
67+
def test_fast_variable_not_found_no_did_you_mean() -> None:
68+
candidates = {"foobar": 1, "foobaz": 2, "fooqux": 3}
69+
with pytest.raises(VariableError) as exc_info:
70+
_fast_variable_not_found("${foobar}", candidates)
71+
72+
message = str(exc_info.value)
73+
assert "Did you mean" not in message
74+
assert "Variable '${foobar}' not found." == message
75+
76+
77+
def test_original_variable_not_found_produces_recommendation() -> None:
78+
"""Verify the original RF function DOES produce recommendations,
79+
confirming the patch actually removes them."""
80+
from functools import partial
81+
82+
from robot.utils import RecommendationFinder, normalize
83+
from robot.variables.notfound import _decorate_candidates
84+
85+
name = "${foobar}"
86+
store = {"foobar": 1, "foobaz": 2, "something_else": 3}
87+
candidates = _decorate_candidates(name[0], store)
88+
normalizer = partial(normalize, ignore="$@&%{}_")
89+
message = RecommendationFinder(normalizer).find_and_format(
90+
name, candidates, message=f"Variable '{name}' not found."
91+
)
92+
assert "Did you mean" in message

0 commit comments

Comments
 (0)