Skip to content

Commit 2dfc91b

Browse files
committed
perf(analyzer): speed up the creation of libdocs from resource files
1 parent 7613bb2 commit 2dfc91b

File tree

7 files changed

+38
-99
lines changed

7 files changed

+38
-99
lines changed

packages/language_server/src/robotcode/language_server/robotframework/parts/code_lens.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,10 @@ def resolve(self, sender: Any, code_lens: CodeLens) -> Optional[CodeLens]:
7070

7171
namespace = self.parent.documents_cache.get_namespace(document)
7272

73-
name = code_lens.data["name"]
7473
line = code_lens.data["line"]
7574

7675
if self.parent.diagnostics.workspace_loaded_event.is_set():
77-
kw_doc = self.get_keyword_definition_at_line(namespace.get_library_doc(), name, line)
76+
kw_doc = self.get_keyword_definition_at_line(namespace.get_library_doc(), line)
7877

7978
if kw_doc is not None and not kw_doc.is_error_handler:
8079
if not self.parent.robot_references.has_cached_keyword_references(

packages/language_server/src/robotcode/language_server/robotframework/parts/robot_workspace.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from concurrent.futures import CancelledError
22
from logging import CRITICAL
3-
from pathlib import Path
43
from threading import Event
5-
from typing import TYPE_CHECKING, Any, List, Optional
4+
from typing import TYPE_CHECKING, Any, Optional
65

76
from robotcode.core.ignore_spec import DEFAULT_SPEC_RULES, GIT_IGNORE_FILE, ROBOT_IGNORE_FILE, IgnoreSpec, iter_files
87
from robotcode.core.language import language_id
@@ -60,8 +59,6 @@ def on_get_analysis_progress_mode(self, sender: Any, uri: Uri) -> Optional[Analy
6059
def load_workspace_documents(self, sender: Any) -> None:
6160
with self._logger.measure_time(lambda: "loading workspace documents", context_name="load_workspace_documents"):
6261
try:
63-
result: List[Path] = []
64-
6562
for folder in self.parent.workspace.workspace_folders:
6663
config = self.parent.workspace.get_configuration(RobotCodeConfig, folder.uri)
6764

@@ -84,9 +81,10 @@ def load_workspace_documents(self, sender: Any) -> None:
8481
)
8582
)
8683

87-
result.extend(files)
88-
8984
canceled = False
85+
self._logger.debug(
86+
lambda: f"Loading {len(files)} workspace documents", context_name="load_workspace_documents"
87+
)
9088
with self.parent.window.progress(
9189
"Load workspace", current=0, max=len(files), start=False, cancellable=False
9290
) as progress:

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

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@
7676
from robotcode.robot.utils.ast import (
7777
cached_isinstance,
7878
get_variable_token,
79+
iter_nodes,
7980
range_from_token,
8081
strip_variable_token,
8182
)
8283
from robotcode.robot.utils.markdownformatter import MarkDownFormatter
8384
from robotcode.robot.utils.match import normalize, normalize_namespace
84-
from robotcode.robot.utils.stubs import HasError, HasErrors
8585

8686
from ..utils.variables import contains_variable
8787

@@ -239,7 +239,6 @@ def __init__(
239239
self._can_have_embedded = can_have_embedded and not is_namespace
240240
self._is_namespace = is_namespace
241241
self._normalized_name: Optional[str] = None
242-
self._embedded_arguments: Any = None
243242

244243
@property
245244
def normalized_name(self) -> str:
@@ -248,15 +247,12 @@ def normalized_name(self) -> str:
248247

249248
return self._normalized_name
250249

251-
@property
250+
@functools.cached_property
252251
def embedded_arguments(self) -> Any:
253-
if self._embedded_arguments is None:
254-
if self._can_have_embedded:
255-
self._embedded_arguments = _get_embedded_arguments(self.name)
256-
else:
257-
self._embedded_arguments = ()
252+
if self._can_have_embedded:
253+
return _get_embedded_arguments(self.name) or ()
258254

259-
return self._embedded_arguments
255+
return ()
260256

261257
if get_robot_version() >= (6, 0):
262258

@@ -269,7 +265,7 @@ def __match_embedded(self, name: str) -> bool:
269265
return self.embedded_arguments.name.match(name) is not None
270266

271267
def __eq__(self, o: object) -> bool:
272-
if cached_isinstance(o, KeywordMatcher):
268+
if type(o) is KeywordMatcher:
273269
if self._is_namespace != o._is_namespace:
274270
return False
275271

@@ -667,13 +663,11 @@ def __post_init__(self) -> None:
667663
def __str__(self) -> str:
668664
return f"{self.name}({', '.join(str(arg) for arg in self.arguments)})"
669665

670-
@property
666+
@functools.cached_property
671667
def matcher(self) -> KeywordMatcher:
672-
if not hasattr(self, "__matcher"):
673-
self.__matcher = KeywordMatcher(self.name)
674-
return self.__matcher
668+
return KeywordMatcher(self.name)
675669

676-
@property
670+
@functools.cached_property
677671
def is_deprecated(self) -> bool:
678672
return self.deprecated or DEPRECATED_PATTERN.match(self.doc) is not None
679673

@@ -685,13 +679,13 @@ def is_resource_keyword(self) -> bool:
685679
def is_library_keyword(self) -> bool:
686680
return self.libtype == "LIBRARY"
687681

688-
@property
682+
@functools.cached_property
689683
def deprecated_message(self) -> str:
690684
if (m := DEPRECATED_PATTERN.match(self.doc)) is not None:
691685
return m.group("message").strip()
692686
return ""
693687

694-
@property
688+
@functools.cached_property
695689
def name_range(self) -> Range:
696690
if self.name_token is not None:
697691
return range_from_token(self.name_token)
@@ -709,7 +703,7 @@ def is_private(self) -> bool:
709703

710704
return "robot:private" in self.normalized_tags()
711705

712-
@property
706+
@functools.cached_property
713707
def range(self) -> Range:
714708
if self.name_token is not None:
715709
return range_from_token(self.name_token)
@@ -820,7 +814,7 @@ def escape_pipe(s: str) -> str:
820814

821815
return result
822816

823-
@property
817+
@functools.cached_property
824818
def signature(self) -> str:
825819
return (
826820
f'({self.type}) "{self.name}": ('
@@ -2716,15 +2710,16 @@ def get_model_doc(
27162710
append_model_errors: bool = True,
27172711
) -> LibraryDoc:
27182712
errors: List[Error] = []
2719-
keyword_name_nodes: List[KeywordName] = []
2720-
keywords_nodes: List[Keyword] = []
2721-
for node in ast.walk(model):
2722-
if isinstance(node, Keyword):
2723-
keywords_nodes.append(node)
2724-
if isinstance(node, KeywordName):
2725-
keyword_name_nodes.append(node)
2726-
2727-
error = node.error if isinstance(node, HasError) else None
2713+
keyword_name_nodes: Dict[int, KeywordName] = {}
2714+
keywords_nodes: Dict[int, Keyword] = {}
2715+
for node in iter_nodes(model):
2716+
if cached_isinstance(node, Keyword):
2717+
node.lineno
2718+
keywords_nodes[node.lineno] = node
2719+
if cached_isinstance(node, KeywordName):
2720+
keyword_name_nodes[node.lineno] = node
2721+
2722+
error = getattr(node, "error", None)
27282723
if error is not None:
27292724
errors.append(
27302725
Error(
@@ -2735,7 +2730,7 @@ def get_model_doc(
27352730
)
27362731
)
27372732
if append_model_errors:
2738-
node_errors = node.errors if isinstance(node, HasErrors) else None
2733+
node_errors = getattr(node, "errors", None)
27392734
if node_errors is not None:
27402735
for e in node_errors:
27412736
errors.append(
@@ -2748,16 +2743,15 @@ def get_model_doc(
27482743
)
27492744

27502745
def get_keyword_name_token_from_line(line: int) -> Optional[Token]:
2751-
for keyword_name in keyword_name_nodes:
2752-
if keyword_name.lineno == line:
2753-
return cast(Token, keyword_name.get_token(RobotToken.KEYWORD_NAME))
2754-
2755-
return None
2746+
keyword_name = keyword_name_nodes.get(line, None)
2747+
if keyword_name is None:
2748+
return None
2749+
return cast(Token, keyword_name.get_token(RobotToken.KEYWORD_NAME))
27562750

27572751
def get_argument_definitions_from_line(
27582752
line: int,
27592753
) -> List[ArgumentDefinition]:
2760-
keyword_node = next((k for k in keywords_nodes if k.lineno == line), None)
2754+
keyword_node = keywords_nodes.get(line, None)
27612755
if keyword_node is None:
27622756
return []
27632757

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -665,12 +665,12 @@ def is_bdd_token(cls, namespace: "Namespace", token: Token) -> bool:
665665

666666
@classmethod
667667
def get_keyword_definition_at_token(cls, library_doc: LibraryDoc, token: Token) -> Optional[KeywordDoc]:
668-
return cls.get_keyword_definition_at_line(library_doc, token.value, token.lineno)
668+
return cls.get_keyword_definition_at_line(library_doc, token.lineno)
669669

670670
@classmethod
671-
def get_keyword_definition_at_line(cls, library_doc: LibraryDoc, value: str, line: int) -> Optional[KeywordDoc]:
671+
def get_keyword_definition_at_line(cls, library_doc: LibraryDoc, line: int) -> Optional[KeywordDoc]:
672672
return next(
673-
(k for k in library_doc.keywords.iter_all(value) if k.line_no == line),
673+
(k for k in library_doc.keywords.keywords if k.line_no == line),
674674
None,
675675
)
676676

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,6 @@ def cached_isinstance(obj: Any, *expected_types: Type[_T]) -> TypeGuard[Union[_T
3838
return False
3939

4040

41-
# def cached_isinstance(obj: Any, *expected_types: type) -> bool:
42-
# try:
43-
# return isinstance(obj, expected_types)
44-
# except TypeError:
45-
# return False
46-
47-
4841
def iter_nodes(node: ast.AST, descendants: bool = True) -> Iterator[ast.AST]:
4942
for _field, value in ast.iter_fields(node):
5043
if cached_isinstance(value, list):

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,4 @@
1-
from __future__ import annotations
2-
3-
from typing import Any, Dict, Iterator, List, Optional, Protocol, Set, runtime_checkable
4-
5-
6-
@runtime_checkable
7-
class HasError(Protocol):
8-
error: Optional[str]
9-
10-
11-
@runtime_checkable
12-
class HasErrors(Protocol):
13-
errors: Optional[List[str]]
14-
15-
16-
@runtime_checkable
17-
class HeaderAndBodyBlock(Protocol):
18-
header: Any
19-
body: List[Any]
1+
from typing import Any, Dict, Iterator, List, Protocol, Set, runtime_checkable
202

213

224
@runtime_checkable

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from abc import ABC
33
from typing import (
44
Any,
5-
AsyncIterator,
65
Callable,
76
Dict,
87
Iterator,
@@ -37,32 +36,6 @@ def iter_field_values(node: ast.AST) -> Iterator[Any]:
3736
pass
3837

3938

40-
def iter_child_nodes(node: ast.AST) -> Iterator[ast.AST]:
41-
for _name, field in iter_fields(node):
42-
if isinstance(field, ast.AST):
43-
yield field
44-
elif isinstance(field, list):
45-
for item in field:
46-
if isinstance(item, ast.AST):
47-
yield item
48-
49-
50-
async def iter_nodes(node: ast.AST) -> AsyncIterator[ast.AST]:
51-
for _name, value in iter_fields(node):
52-
if isinstance(value, list):
53-
for item in value:
54-
if isinstance(item, ast.AST):
55-
yield item
56-
async for n in iter_nodes(item):
57-
yield n
58-
59-
elif isinstance(value, ast.AST):
60-
yield value
61-
62-
async for n in iter_nodes(value):
63-
yield n
64-
65-
6639
class VisitorFinder(ABC):
6740
__cls_finder_cache__: Dict[Type[Any], Optional[Callable[..., Any]]]
6841

0 commit comments

Comments
 (0)