Skip to content

Commit 4f37419

Browse files
committed
refactor(robot): extract Namespace into DTO with event-driven invalidation
Split the monolithic Namespace class into focused modules to reduce complexity and improve maintainability. The Namespace is now a pure data container built by NamespaceBuilder, with dedicated modules for import resolution, AST analysis, variable scoping, and scope trees.
1 parent fa2217e commit 4f37419

File tree

16 files changed

+1507
-1858
lines changed

16 files changed

+1507
-1858
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def resolve_code_action_assign_result_to_variable(
502502
counter = 0
503503
namespace = self.parent.documents_cache.get_namespace(document)
504504
while True:
505-
if namespace.find_variable(f"${{{var_name}}}", nodes, range.start, ignore_error=True) is None:
505+
if namespace.find_variable(f"${{{var_name}}}", range.start) is None:
506506
break
507507
counter += 1
508508
var_name = f"new_variable_{counter}"

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ def resolve(self, completion_item: CompletionItem) -> CompletionItem:
493493

494494
elif (name := data.get("name", None)) is not None:
495495
try:
496-
lib_doc = self.namespace.imports_manager.get_libdoc_for_resource_import(
496+
lib_doc = self.namespace.imports_manager.get_resource_doc_for_resource_import(
497497
name,
498498
str(document.uri.to_path().parent),
499499
variables=self.namespace.get_resolvable_variables(),
@@ -623,7 +623,7 @@ def create_variables_completion_items(
623623
text_edit=TextEdit(range=range, new_text=s.name[2:-1]),
624624
filter_text=s.name[2:-1] if range is not None else None,
625625
)
626-
for s in (self.namespace.get_variable_matchers(list(reversed(nodes)), position)).values()
626+
for s in (self.namespace.get_variable_matchers(position)).values()
627627
if s.name is not None and (s.name_token is None or not position.is_in_range(range_from_token(s.name_token)))
628628
]
629629

@@ -1705,7 +1705,7 @@ def complete_import() -> Optional[List[CompletionItem]]:
17051705
complete_list = self.namespace.imports_manager.complete_library_import(
17061706
first_part if first_part else None,
17071707
str(self.document.uri.to_path().parent),
1708-
self.namespace.get_resolvable_variables(nodes_at_position, position),
1708+
self.namespace.get_resolvable_variables(position),
17091709
)
17101710
if not complete_list:
17111711
return None
@@ -1907,7 +1907,7 @@ def complete_ResourceImport( # noqa: N802
19071907
complete_list = self.namespace.imports_manager.complete_resource_import(
19081908
first_part if first_part else None,
19091909
str(self.document.uri.to_path().parent),
1910-
self.namespace.get_resolvable_variables(nodes_at_position, position),
1910+
self.namespace.get_resolvable_variables(position),
19111911
)
19121912
if not complete_list:
19131913
return None
@@ -2031,7 +2031,7 @@ def complete_import() -> Optional[List[CompletionItem]]:
20312031
complete_list = self.namespace.imports_manager.complete_variables_import(
20322032
first_part if first_part else None,
20332033
str(self.document.uri.to_path().parent),
2034-
self.namespace.get_resolvable_variables(nodes_at_position, position),
2034+
self.namespace.get_resolvable_variables(position),
20352035
)
20362036
if not complete_list:
20372037
return None

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async def _get_evaluatable_expression(
6969
token_and_var = next(
7070
(
7171
(t, v)
72-
for t, v in self.iter_variables_from_token(token, namespace, nodes, position)
72+
for t, v in self.iter_variables_from_token(token, namespace, position)
7373
if position in range_from_token(t)
7474
),
7575
None,
@@ -84,7 +84,7 @@ async def _get_evaluatable_expression(
8484
token_and_var = next(
8585
(
8686
(var_token, var)
87-
for var_token, var in self.iter_expression_variables_from_token(token, namespace, nodes, position)
87+
for var_token, var in self.iter_expression_variables_from_token(token, namespace, position)
8888
if position in range_from_token(var_token)
8989
),
9090
None,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def _on_initialized(self, sender: Any) -> None:
5151
self.parent.diagnostics.analyze.add(self.analyze)
5252
self.parent.documents_cache.namespace_initialized(self._on_namespace_initialized)
5353
self.parent.documents_cache.libraries_changed.add(self._on_libraries_changed)
54+
self.parent.documents_cache.resources_changed.add(self._on_resources_changed)
5455
self.parent.documents_cache.variables_changed.add(self._on_variables_changed)
5556

5657
def _on_libraries_changed(self, sender: Any, libraries: List[LibraryDoc]) -> None:
@@ -61,6 +62,14 @@ def _on_libraries_changed(self, sender: Any, libraries: List[LibraryDoc]) -> Non
6162
if any(lib_doc in lib_docs for lib_doc in libraries):
6263
self.parent.diagnostics.force_refresh_document(doc)
6364

65+
def _on_resources_changed(self, sender: Any, resources: List[LibraryDoc]) -> None:
66+
for doc in self.parent.documents.documents:
67+
namespace = self.parent.documents_cache.get_only_initialized_namespace(doc)
68+
if namespace is not None:
69+
lib_docs = (e.library_doc for e in namespace.get_resources().values())
70+
if any(lib_doc.source == changed.source for lib_doc in lib_docs for changed in resources):
71+
self.parent.diagnostics.force_refresh_document(doc)
72+
6473
def _on_variables_changed(self, sender: Any, variables: List[LibraryDoc]) -> None:
6574
for doc in self.parent.documents.documents:
6675
namespace = self.parent.documents_cache.get_only_initialized_namespace(doc)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def _hover_default(self, nodes: List[ast.AST], document: TextDocument, position:
143143
real_value = namespace.imports_manager.resolve_variable(
144144
variable.name,
145145
str(document.uri.to_path().parent),
146-
namespace.get_resolvable_variables(nodes, position),
146+
namespace.get_resolvable_variables(position),
147147
)
148148

149149
value = _my_repr.repr(real_value)

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from robotcode.core.utils.logging import LoggingDescriptor
1717
from robotcode.robot.diagnostics.model_helper import ModelHelper
1818
from robotcode.robot.utils.ast import (
19-
get_nodes_at_position,
2019
iter_nodes,
2120
range_from_node,
2221
range_from_token,
@@ -55,8 +54,6 @@ def collect(
5554

5655
real_range = Range(range.start, min(range.end, context.stopped_location.end))
5756

58-
nodes = get_nodes_at_position(model, context.stopped_location.start)
59-
6057
def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
6158
for n in iter_nodes(model):
6259
r = range_from_node(n)
@@ -76,12 +73,12 @@ def get_tokens() -> Iterator[Tuple[Token, ast.AST]]:
7673
):
7774
if token.type == RobotToken.ARGUMENT and isinstance(node, self.get_expression_statement_types()):
7875
for t, var in self.iter_expression_variables_from_token(
79-
token, namespace, nodes, context.stopped_location.start
76+
token, namespace, context.stopped_location.start
8077
):
8178
if var.name != "${CURDIR}":
8279
result.append(InlineValueEvaluatableExpression(range_from_token(t), var.name))
8380

84-
for t, var in self.iter_variables_from_token(token, namespace, nodes, context.stopped_location.start):
81+
for t, var in self.iter_variables_from_token(token, namespace, context.stopped_location.start):
8582
if var.name != "${CURDIR}":
8683
result.append(InlineValueEvaluatableExpression(range_from_token(t), var.name))
8784

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def collect(self, sender: Any, document: TextDocument, positions: List[Position]
5555
for var_token, _ in self.iter_variables_from_token(
5656
token,
5757
namespace,
58-
nodes,
5958
position,
6059
return_not_found=True,
6160
):

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from ..utils.stubs import Languages
3737
from .imports_manager import ImportsManager
3838
from .library_doc import LibraryDoc
39-
from .namespace import DocumentType, Namespace
39+
from .namespace import DocumentType, Namespace, NamespaceBuilder
4040
from .workspace_config import (
4141
AnalysisDiagnosticModifiersConfig,
4242
AnalysisRobotConfig,
@@ -351,7 +351,7 @@ def namespace_initialized(sender, namespace: Namespace) -> None: ...
351351
@event
352352
def namespace_invalidated(sender, namespace: Namespace) -> None: ...
353353

354-
def __invalidate_namespace(self, sender: Namespace) -> None:
354+
def _invalidate_namespace(self, sender: Namespace) -> None:
355355
document = sender.document
356356
if document is not None:
357357
document.remove_cache_entry(self.__get_general_namespace)
@@ -360,10 +360,10 @@ def __invalidate_namespace(self, sender: Namespace) -> None:
360360

361361
self.namespace_invalidated(self, sender)
362362

363-
def __namespace_initialized(self, sender: Namespace) -> None:
364-
if sender.document is not None:
365-
sender.document.set_data(self.INITIALIZED_NAMESPACE, sender)
366-
self.namespace_initialized(self, sender)
363+
def __namespace_initialized(self, namespace: Namespace) -> None:
364+
if namespace.document is not None:
365+
namespace.document.set_data(self.INITIALIZED_NAMESPACE, namespace)
366+
self.namespace_initialized(self, namespace)
367367

368368
def get_initialized_namespace(self, document: TextDocument) -> Namespace:
369369
result: Optional[Namespace] = document.get_data(self.INITIALIZED_NAMESPACE)
@@ -391,7 +391,7 @@ def __get_namespace_for_document_type(
391391

392392
languages, workspace_languages = self.build_languages_from_model(document, model)
393393

394-
result = Namespace(
394+
builder = NamespaceBuilder(
395395
imports_manager,
396396
model,
397397
str(document.uri.to_path()),
@@ -400,8 +400,15 @@ def __get_namespace_for_document_type(
400400
languages,
401401
workspace_languages,
402402
)
403-
result.has_invalidated.add(self.__invalidate_namespace)
404-
result.has_initialized.add(self.__namespace_initialized)
403+
404+
result = builder.build()
405+
406+
# When the namespace detects dependency changes, evict the
407+
# document cache entry so it gets rebuilt on next access.
408+
result.invalidated.add(self._invalidate_namespace)
409+
410+
# Mark as initialized for consumers that need early access
411+
self.__namespace_initialized(result)
405412

406413
return result
407414

0 commit comments

Comments
 (0)