robotcode-robot 2.8.0.dev631__py3-none-any.whl → 2.8.0.dev677__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/config/model.py +42 -35
- robotcode/robot/diagnostics/analyzer_result.py +5 -1
- robotcode/robot/diagnostics/document_cache_helper.py +10 -0
- robotcode/robot/diagnostics/entities.py +20 -4
- robotcode/robot/diagnostics/import_resolver.py +1 -14
- robotcode/robot/diagnostics/imports_manager.py +34 -19
- robotcode/robot/diagnostics/namespace.py +25 -34
- robotcode/robot/diagnostics/namespace_analyzer.py +11 -1
- robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +11 -1
- robotcode/robot/diagnostics/semantic_analyzer/model.py +41 -1
- robotcode/robot/diagnostics/semantic_analyzer/nodes.py +49 -1
- robotcode/robot/diagnostics/semantic_analyzer/serialization.py +25 -11
- robotcode/robot/utils/variables.py +9 -0
- {robotcode_robot-2.8.0.dev631.dist-info → robotcode_robot-2.8.0.dev677.dist-info}/METADATA +2 -2
- {robotcode_robot-2.8.0.dev631.dist-info → robotcode_robot-2.8.0.dev677.dist-info}/RECORD +17 -17
- {robotcode_robot-2.8.0.dev631.dist-info → robotcode_robot-2.8.0.dev677.dist-info}/WHEEL +0 -0
robotcode/robot/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.8.0-dev.
|
|
1
|
+
__version__ = "2.8.0-dev.677"
|
robotcode/robot/config/model.py
CHANGED
|
@@ -3398,47 +3398,54 @@ class RobotConfig(RobotExtendBaseProfile):
|
|
|
3398
3398
|
else:
|
|
3399
3399
|
verbose_callback("No profiles selected.")
|
|
3400
3400
|
|
|
3401
|
+
# whether a selected profile is enabled; None when its condition has an error, which is reported only once
|
|
3402
|
+
enabled_states: Dict[str, Optional[bool]] = {}
|
|
3403
|
+
|
|
3404
|
+
def is_enabled(name: str) -> Optional[bool]:
|
|
3405
|
+
if name not in enabled_states:
|
|
3406
|
+
try:
|
|
3407
|
+
enabled_states[name] = selected_profiles[name][0].is_enabled()
|
|
3408
|
+
except EvaluationError as e:
|
|
3409
|
+
message = f"Error evaluating 'enabled' condition for profile '{name}': {e}"
|
|
3410
|
+
|
|
3411
|
+
if error_callback is None:
|
|
3412
|
+
raise ValueError(message) from e
|
|
3413
|
+
|
|
3414
|
+
error_callback(message)
|
|
3415
|
+
enabled_states[name] = None
|
|
3416
|
+
return enabled_states[name]
|
|
3417
|
+
|
|
3418
|
+
# a profile is used when it is enabled and selected itself, or when one of the profiles that inherit it is
|
|
3419
|
+
# used, so that a disabled profile brings none of the profiles it inherits from along, at any depth
|
|
3420
|
+
used_states: Dict[str, bool] = {}
|
|
3421
|
+
|
|
3422
|
+
def is_used(name: str) -> bool:
|
|
3423
|
+
if name not in used_states:
|
|
3424
|
+
parents = selected_profiles[name][1]
|
|
3425
|
+
used_states[name] = bool(is_enabled(name)) and (
|
|
3426
|
+
parents is None or any(is_used(parent_name) for parent_name, _ in parents)
|
|
3427
|
+
)
|
|
3428
|
+
return used_states[name]
|
|
3429
|
+
|
|
3401
3430
|
for profile_name, (profile, parent_profiles) in sorted(
|
|
3402
3431
|
selected_profiles.items(), key=lambda x: x[1][0].precedence or 0
|
|
3403
3432
|
):
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
if verbose_callback:
|
|
3407
|
-
verbose_callback(f'Skipping profile "{profile_name}" because it\'s disabled.')
|
|
3408
|
-
continue
|
|
3409
|
-
except EvaluationError as e:
|
|
3410
|
-
message = f"Error evaluating 'enabled' condition for profile '{profile_name}': {e}"
|
|
3411
|
-
|
|
3412
|
-
if error_callback is None:
|
|
3413
|
-
raise ValueError(message) from e
|
|
3414
|
-
|
|
3415
|
-
error_callback(message)
|
|
3433
|
+
profile_enabled = is_enabled(profile_name)
|
|
3434
|
+
if profile_enabled is None:
|
|
3416
3435
|
continue
|
|
3417
3436
|
|
|
3418
|
-
if
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
try:
|
|
3423
|
-
if not parent_profile[1].is_enabled():
|
|
3424
|
-
disabled_profiles.append(parent_profile[0])
|
|
3425
|
-
except EvaluationError as e:
|
|
3426
|
-
message = f"Error evaluating 'enabled' condition for profile '{parent_profile[0]}': {e}"
|
|
3427
|
-
|
|
3428
|
-
if error_callback is None:
|
|
3429
|
-
raise ValueError(message) from e
|
|
3430
|
-
|
|
3431
|
-
error_callback(message)
|
|
3432
|
-
skip = True
|
|
3433
|
-
break
|
|
3437
|
+
if not profile_enabled:
|
|
3438
|
+
if verbose_callback:
|
|
3439
|
+
verbose_callback(f'Skipping profile "{profile_name}" because it\'s disabled.')
|
|
3440
|
+
continue
|
|
3434
3441
|
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
+
if not is_used(profile_name):
|
|
3443
|
+
if verbose_callback:
|
|
3444
|
+
verbose_callback(
|
|
3445
|
+
f"Skipping inherited profile '{profile_name}' because none of the profiles that inherit it"
|
|
3446
|
+
f" is used: {', '.join(sorted(name for name, _ in parent_profiles or ()))}."
|
|
3447
|
+
)
|
|
3448
|
+
continue
|
|
3442
3449
|
|
|
3443
3450
|
if verbose_callback:
|
|
3444
3451
|
verbose_callback(f'Using profile "{profile_name}".')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
|
3
3
|
|
|
4
4
|
from robotcode.core.lsp.types import Diagnostic, Location, Range
|
|
@@ -25,3 +25,7 @@ class AnalyzerResult:
|
|
|
25
25
|
testcase_metadata_references: Dict[str, Set[Location]]
|
|
26
26
|
scope_tree: ScopeTree
|
|
27
27
|
semantic_model: Optional["SemanticModel"] = None
|
|
28
|
+
# the environment variables the analysis read, which the namespace cache compares when it restores the analysis:
|
|
29
|
+
# whether each one checked without a default value is set, and the hash of each one whose value the analysis used
|
|
30
|
+
environment_set: Dict[str, bool] = field(default_factory=dict)
|
|
31
|
+
environment_values: Dict[str, Optional[str]] = field(default_factory=dict)
|
|
@@ -640,6 +640,16 @@ class DocumentsCacheHelper:
|
|
|
640
640
|
)
|
|
641
641
|
return None
|
|
642
642
|
|
|
643
|
+
# from_data resolved the imports again with the current Python path and environment variables. A namespace
|
|
644
|
+
# with a failed import is never stored and the dependency keys name the files found, so a failed import or
|
|
645
|
+
# other keys mean that an import finds another file or none now.
|
|
646
|
+
if result.failed_imports or set(result.dependency_metas or {}) != set(meta.dependency_fingerprints):
|
|
647
|
+
self._logger.debug(
|
|
648
|
+
lambda: f"Cache miss for {source}: the imports find other files now, re-analyzing",
|
|
649
|
+
context_name="cache",
|
|
650
|
+
)
|
|
651
|
+
return None
|
|
652
|
+
|
|
643
653
|
self._logger.debug(
|
|
644
654
|
lambda: f"Loaded namespace from disk cache for {source}",
|
|
645
655
|
context_name="import",
|
|
@@ -52,10 +52,6 @@ class Import(SourceEntity):
|
|
|
52
52
|
name: Optional[str]
|
|
53
53
|
name_token: Optional[Token]
|
|
54
54
|
|
|
55
|
-
@property
|
|
56
|
-
def hint_key(self) -> str:
|
|
57
|
-
return f"{self.name}\0{self.source or ''}\0{self.line_no}"
|
|
58
|
-
|
|
59
55
|
@property
|
|
60
56
|
def range(self) -> Range:
|
|
61
57
|
return Range(
|
|
@@ -360,6 +356,26 @@ class VariablesEntry(LibraryEntry):
|
|
|
360
356
|
)
|
|
361
357
|
|
|
362
358
|
|
|
359
|
+
def library_entry_key(entry: LibraryEntry) -> str:
|
|
360
|
+
"""The key of a library entry in the namespace disk cache: of a
|
|
361
|
+
`namespace_references` entry in `NamespaceData` and of a `lib_entry` in
|
|
362
|
+
a pickled semantic model.
|
|
363
|
+
|
|
364
|
+
Like `LibraryEntry.__hash__`, it includes the import position, so it tells
|
|
365
|
+
apart the entries of one library: the implicitly imported BuiltIn and an
|
|
366
|
+
explicit import of it, or a library imported by a resource file and the
|
|
367
|
+
same library imported directly.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
def position(r: Range) -> str:
|
|
371
|
+
return f"{r.start.line}.{r.start.character}-{r.end.line}.{r.end.character}"
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
f"{type(entry).__name__}:{entry.name}:{entry.import_name}:{entry.args!r}:{entry.alias or ''}"
|
|
375
|
+
f":{entry.import_source or ''}:{position(entry.import_range)}:{position(entry.alias_range)}"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
363
379
|
@dataclass(slots=True)
|
|
364
380
|
class TestCaseDefinition(SourceEntity):
|
|
365
381
|
name: str
|
|
@@ -9,7 +9,6 @@ The resolver takes imports + a VariableScope and returns ResolvedImports
|
|
|
9
9
|
containing all resolved entries and collected diagnostics.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import os
|
|
13
12
|
from dataclasses import dataclass, field
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|
@@ -129,7 +128,6 @@ class ImportResolver:
|
|
|
129
128
|
"_scope",
|
|
130
129
|
"_sentinel",
|
|
131
130
|
"_source",
|
|
132
|
-
"_source_hints",
|
|
133
131
|
"_source_id",
|
|
134
132
|
"_variables",
|
|
135
133
|
"_variables_dirty",
|
|
@@ -159,14 +157,11 @@ class ImportResolver:
|
|
|
159
157
|
self._diagnostics: List[Diagnostic] = []
|
|
160
158
|
self._variables: Dict[str, Any] = {}
|
|
161
159
|
self._variables_dirty = True
|
|
162
|
-
self._source_hints: Dict[str, str] = {}
|
|
163
160
|
self._dependency_metas: Dict[str, Optional[Any]] = {}
|
|
164
161
|
self._failed_imports: List[FailedImport] = []
|
|
165
162
|
|
|
166
|
-
def resolve(self, imports: Iterable[Import]
|
|
163
|
+
def resolve(self, imports: Iterable[Import]) -> ResolvedImports:
|
|
167
164
|
"""Resolve all imports and return the result."""
|
|
168
|
-
if source_hints:
|
|
169
|
-
self._source_hints = source_hints
|
|
170
165
|
self._refresh_variables()
|
|
171
166
|
self._import_default_libraries()
|
|
172
167
|
self._handle_imports(imports, self._base_dir, top_level=True)
|
|
@@ -542,19 +537,11 @@ class ImportResolver:
|
|
|
542
537
|
) -> None:
|
|
543
538
|
assert imp.name is not None
|
|
544
539
|
|
|
545
|
-
# Look up cached source hint to skip find_resource() filesystem search
|
|
546
|
-
source_hint: Optional[str] = None
|
|
547
|
-
if self._source_hints:
|
|
548
|
-
hint = self._source_hints.get(imp.hint_key)
|
|
549
|
-
if hint is not None and os.path.isfile(hint):
|
|
550
|
-
source_hint = hint
|
|
551
|
-
|
|
552
540
|
resource_doc, meta = self._imports_manager.get_resource_doc_for_resource_import_with_meta(
|
|
553
541
|
imp.name,
|
|
554
542
|
base_dir,
|
|
555
543
|
sentinel=self._sentinel,
|
|
556
544
|
variables=self._variables,
|
|
557
|
-
source=source_hint,
|
|
558
545
|
)
|
|
559
546
|
if resource_doc.source is not None:
|
|
560
547
|
# setdefault: on re-imports of the same resource the meta captured
|
|
@@ -59,7 +59,7 @@ from ..__version__ import __version__
|
|
|
59
59
|
from ..utils import RF_VERSION
|
|
60
60
|
from ..utils.robot_path import find_file_ex
|
|
61
61
|
from ..utils.stubs import Languages
|
|
62
|
-
from ..utils.variables import contains_variable
|
|
62
|
+
from ..utils.variables import contains_variable, environment_value_hash
|
|
63
63
|
from .data_cache import CACHE_DIR_NAME, CacheSection, build_cache_dir
|
|
64
64
|
from .data_cache import SqliteDataCache as DefaultDataCache
|
|
65
65
|
from .entities import (
|
|
@@ -722,6 +722,10 @@ class NamespaceMetaData:
|
|
|
722
722
|
config_fingerprint: Any
|
|
723
723
|
dependency_fingerprints: Dict[str, Any] = field(default_factory=dict)
|
|
724
724
|
semantic_model_enabled: bool = False
|
|
725
|
+
# the environment variables the analysis read: whether each one checked without a default value was set, and the
|
|
726
|
+
# hash of each one whose value it used (see environment_value_hash)
|
|
727
|
+
environment_set: Dict[str, bool] = field(default_factory=dict)
|
|
728
|
+
environment_values: Dict[str, Optional[str]] = field(default_factory=dict)
|
|
725
729
|
|
|
726
730
|
|
|
727
731
|
class ImportsManager:
|
|
@@ -808,7 +812,6 @@ class ImportsManager:
|
|
|
808
812
|
self._config_fingerprint: Any = (
|
|
809
813
|
tuple(sorted(self.cmd_variables.items())),
|
|
810
814
|
tuple(self.cmd_variable_files),
|
|
811
|
-
tuple(sorted((k, v) for k, v in self._environment.items() if k not in os.environ)),
|
|
812
815
|
tuple(self.global_library_search_order),
|
|
813
816
|
self._languages_fingerprint,
|
|
814
817
|
)
|
|
@@ -895,8 +898,9 @@ class ImportsManager:
|
|
|
895
898
|
"""Cached configuration snapshot.
|
|
896
899
|
|
|
897
900
|
Computed once at init from cmd_variables, cmd_variable_files,
|
|
898
|
-
|
|
899
|
-
detected via direct ``==`` comparison.
|
|
901
|
+
global_library_search_order and the language settings. Changes are
|
|
902
|
+
detected via direct ``==`` comparison. Environment variables are
|
|
903
|
+
not part of it: each namespace keeps the ones its analysis read.
|
|
900
904
|
"""
|
|
901
905
|
return self._config_fingerprint
|
|
902
906
|
|
|
@@ -949,13 +953,15 @@ class ImportsManager:
|
|
|
949
953
|
config_fingerprint=self.config_fingerprint,
|
|
950
954
|
dependency_fingerprints=dict(dependency_metas),
|
|
951
955
|
semantic_model_enabled=semantic_model_enabled,
|
|
956
|
+
environment_set=dict(namespace.environment_set),
|
|
957
|
+
environment_values=dict(namespace.environment_values),
|
|
952
958
|
)
|
|
953
959
|
|
|
954
960
|
def validate_namespace_meta(self, meta: NamespaceMetaData, source_disk_info: Optional[DiskInfo]) -> bool:
|
|
955
961
|
"""Check whether a cached NamespaceMetaData is fresh for the given content.
|
|
956
962
|
|
|
957
963
|
Performs a 2-level validation:
|
|
958
|
-
Level 1 (fast): source_disk_info, config_fingerprint
|
|
964
|
+
Level 1 (fast): source_disk_info, config_fingerprint, the environment variables the analysis read
|
|
959
965
|
Level 2 (dependency check): each dependency fingerprint
|
|
960
966
|
|
|
961
967
|
Validation is against the *effective* content: the source is compared
|
|
@@ -990,6 +996,25 @@ class ImportsManager:
|
|
|
990
996
|
)
|
|
991
997
|
return False
|
|
992
998
|
|
|
999
|
+
# the analysis reads os.environ itself, so it does not matter where a variable was set
|
|
1000
|
+
for env_name, was_set in meta.environment_set.items():
|
|
1001
|
+
if (os.environ.get(env_name) is not None) != was_set:
|
|
1002
|
+
name = env_name
|
|
1003
|
+
self._logger.debug(
|
|
1004
|
+
lambda: f"Cache miss for {source}: environment variable {name!r} was set or unset",
|
|
1005
|
+
context_name="cache",
|
|
1006
|
+
)
|
|
1007
|
+
return False
|
|
1008
|
+
|
|
1009
|
+
for env_name, value_hash in meta.environment_values.items():
|
|
1010
|
+
if environment_value_hash(os.environ.get(env_name)) != value_hash:
|
|
1011
|
+
name = env_name
|
|
1012
|
+
self._logger.debug(
|
|
1013
|
+
lambda: f"Cache miss for {source}: environment variable {name!r} changed",
|
|
1014
|
+
context_name="cache",
|
|
1015
|
+
)
|
|
1016
|
+
return False
|
|
1017
|
+
|
|
993
1018
|
# Level 2: dependency checks — direct comparison, no hashing
|
|
994
1019
|
for key, saved_value in meta.dependency_fingerprints.items():
|
|
995
1020
|
if key.startswith("lib:"):
|
|
@@ -2211,10 +2236,8 @@ class ImportsManager:
|
|
|
2211
2236
|
base_dir: str,
|
|
2212
2237
|
sentinel: Any = None,
|
|
2213
2238
|
variables: Optional[Dict[str, Any]] = None,
|
|
2214
|
-
*,
|
|
2215
|
-
source: Optional[str] = None,
|
|
2216
2239
|
) -> _ResourcesEntry:
|
|
2217
|
-
source =
|
|
2240
|
+
source = self.find_resource(name, base_dir, variables=variables)
|
|
2218
2241
|
normalized_source = os.path.normpath(os.path.abspath(source))
|
|
2219
2242
|
|
|
2220
2243
|
entry_key = _ResourcesEntryKey(normalized_source)
|
|
@@ -2238,12 +2261,10 @@ class ImportsManager:
|
|
|
2238
2261
|
base_dir: str,
|
|
2239
2262
|
sentinel: Any = None,
|
|
2240
2263
|
variables: Optional[Dict[str, Any]] = None,
|
|
2241
|
-
*,
|
|
2242
|
-
source: Optional[str] = None,
|
|
2243
2264
|
) -> Tuple["Namespace", LibraryDoc]:
|
|
2244
2265
|
with self._logger.measure_time(lambda: f"getting namespace and libdoc for {name}", context_name="import"):
|
|
2245
2266
|
with self._logger.measure_time(lambda: f"getting resource entry {name}", context_name="import"):
|
|
2246
|
-
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables
|
|
2267
|
+
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
|
|
2247
2268
|
|
|
2248
2269
|
with self._logger.measure_time(lambda: f"getting namespace {name}", context_name="import"):
|
|
2249
2270
|
namespace = entry.get_namespace()
|
|
@@ -2258,11 +2279,9 @@ class ImportsManager:
|
|
|
2258
2279
|
base_dir: str,
|
|
2259
2280
|
sentinel: Any = None,
|
|
2260
2281
|
variables: Optional[Dict[str, Any]] = None,
|
|
2261
|
-
*,
|
|
2262
|
-
source: Optional[str] = None,
|
|
2263
2282
|
) -> Tuple[ResourceDoc, Optional[RobotFileMeta]]:
|
|
2264
2283
|
with self._logger.measure_time(lambda: f"getting resource doc for {name}", context_name="import"):
|
|
2265
|
-
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables
|
|
2284
|
+
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables)
|
|
2266
2285
|
|
|
2267
2286
|
return entry.get_resource_doc_with_meta()
|
|
2268
2287
|
|
|
@@ -2272,12 +2291,8 @@ class ImportsManager:
|
|
|
2272
2291
|
base_dir: str,
|
|
2273
2292
|
sentinel: Any = None,
|
|
2274
2293
|
variables: Optional[Dict[str, Any]] = None,
|
|
2275
|
-
*,
|
|
2276
|
-
source: Optional[str] = None,
|
|
2277
2294
|
) -> ResourceDoc:
|
|
2278
|
-
return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables
|
|
2279
|
-
0
|
|
2280
|
-
]
|
|
2295
|
+
return self.get_resource_doc_for_resource_import_with_meta(name, base_dir, sentinel, variables)[0]
|
|
2281
2296
|
|
|
2282
2297
|
def complete_library_import(
|
|
2283
2298
|
self,
|
|
@@ -44,6 +44,7 @@ from .entities import (
|
|
|
44
44
|
VariableDefinition,
|
|
45
45
|
VariablesEntry,
|
|
46
46
|
VariablesImport,
|
|
47
|
+
library_entry_key,
|
|
47
48
|
)
|
|
48
49
|
from .errors import DIAGNOSTICS_SOURCE_NAME
|
|
49
50
|
from .import_resolver import FailedImport, ImportResolver
|
|
@@ -132,12 +133,6 @@ class NamespaceData:
|
|
|
132
133
|
# --- ScopeTree (local scopes only, file_scope is reconstructed) ---
|
|
133
134
|
local_scopes: List[LocalScope] = field(default_factory=list)
|
|
134
135
|
|
|
135
|
-
# --- Resolved resource import sources ---
|
|
136
|
-
# Maps (import_name\0source_file) → resolved absolute path for resources.
|
|
137
|
-
# Used as source hints in from_data() to skip find_resource() filesystem
|
|
138
|
-
# lookups during import re-resolution.
|
|
139
|
-
resolved_resource_sources: Dict[str, str] = field(default_factory=dict)
|
|
140
|
-
|
|
141
136
|
# --- Authoritative variable definitions (stable_id → VariableDefinition) ---
|
|
142
137
|
# Stores the exact VariableDefinition objects that were referenced during
|
|
143
138
|
# analysis. Used as fallback in from_data() when the re-parsed resource doc
|
|
@@ -147,24 +142,6 @@ class NamespaceData:
|
|
|
147
142
|
variable_definitions: Dict[str, VariableDefinition] = field(default_factory=dict)
|
|
148
143
|
|
|
149
144
|
|
|
150
|
-
def _namespace_reference_key(entry: LibraryEntry) -> str:
|
|
151
|
-
"""The key of a `namespace_references` entry in `NamespaceData`.
|
|
152
|
-
|
|
153
|
-
Like `LibraryEntry.__hash__`, it includes the import position, so it tells
|
|
154
|
-
apart the entries of one library: the implicitly imported BuiltIn and an
|
|
155
|
-
explicit import of it, or a library imported by a resource file and the
|
|
156
|
-
same library imported directly.
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
def position(r: Range) -> str:
|
|
160
|
-
return f"{r.start.line}.{r.start.character}-{r.end.line}.{r.end.character}"
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
f"{type(entry).__name__}:{entry.name}:{entry.import_name}:{entry.args!r}:{entry.alias or ''}"
|
|
164
|
-
f":{entry.import_source or ''}:{position(entry.import_range)}:{position(entry.alias_range)}"
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
145
|
class Namespace:
|
|
169
146
|
"""Data container holding all results of a namespace build.
|
|
170
147
|
|
|
@@ -206,6 +183,8 @@ class Namespace:
|
|
|
206
183
|
semantic_model: Optional["SemanticModel"] = None,
|
|
207
184
|
dependency_metas: Optional[Dict[str, Optional[Any]]] = None,
|
|
208
185
|
failed_imports: Optional[List[FailedImport]] = None,
|
|
186
|
+
environment_set: Optional[Dict[str, bool]] = None,
|
|
187
|
+
environment_values: Optional[Dict[str, Optional[str]]] = None,
|
|
209
188
|
) -> None:
|
|
210
189
|
self.imports_manager = imports_manager
|
|
211
190
|
self.source = source
|
|
@@ -236,6 +215,8 @@ class Namespace:
|
|
|
236
215
|
self._semantic_model = semantic_model
|
|
237
216
|
self._dependency_metas = dependency_metas
|
|
238
217
|
self._failed_imports = failed_imports or []
|
|
218
|
+
self._environment_set = environment_set or {}
|
|
219
|
+
self._environment_values = environment_values or {}
|
|
239
220
|
|
|
240
221
|
# Lazy-computed caches
|
|
241
222
|
self._namespaces: Optional[Dict[KeywordMatcher, List[LibraryEntry]]] = None
|
|
@@ -261,6 +242,16 @@ class Namespace:
|
|
|
261
242
|
"""The imports whose resolution raised or whose library or variables documentation has errors."""
|
|
262
243
|
return self._failed_imports
|
|
263
244
|
|
|
245
|
+
@property
|
|
246
|
+
def environment_set(self) -> Dict[str, bool]:
|
|
247
|
+
"""Whether each environment variable the analysis checked without a default value was set."""
|
|
248
|
+
return self._environment_set
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def environment_values(self) -> Dict[str, Optional[str]]:
|
|
252
|
+
"""The hash of each environment variable whose value the analysis used, see `environment_value_hash`."""
|
|
253
|
+
return self._environment_values
|
|
254
|
+
|
|
264
255
|
@property
|
|
265
256
|
def document(self) -> Optional[TextDocument]:
|
|
266
257
|
return self._document() if self._document is not None else None
|
|
@@ -516,7 +507,7 @@ class Namespace:
|
|
|
516
507
|
"""
|
|
517
508
|
# Build namespace_references key from LibraryEntry identity
|
|
518
509
|
ns_refs: Dict[str, Set[Location]] = {
|
|
519
|
-
|
|
510
|
+
library_entry_key(entry): locs for entry, locs in self._namespace_references.items()
|
|
520
511
|
}
|
|
521
512
|
|
|
522
513
|
# Collect all referenced variable definitions for stable_id → object lookup
|
|
@@ -555,12 +546,6 @@ class Namespace:
|
|
|
555
546
|
else:
|
|
556
547
|
var_assigns_merged[sid] = set(ranges)
|
|
557
548
|
|
|
558
|
-
# Build resolved resource source hints for from_data() optimization
|
|
559
|
-
resolved_res_sources: Dict[str, str] = {}
|
|
560
|
-
for imp, entry in self._import_entries.items():
|
|
561
|
-
if isinstance(entry, ResourceEntry) and entry.library_doc.source:
|
|
562
|
-
resolved_res_sources[imp.hint_key] = entry.library_doc.source
|
|
563
|
-
|
|
564
549
|
return NamespaceData(
|
|
565
550
|
source=self.source,
|
|
566
551
|
source_id=str(self.source_id) if self.source_id else None,
|
|
@@ -580,7 +565,6 @@ class Namespace:
|
|
|
580
565
|
metadata_references={k: set(v) for k, v in self._metadata_references.items()},
|
|
581
566
|
testcase_metadata_references={k: set(v) for k, v in self._testcase_metadata_references.items()},
|
|
582
567
|
local_scopes=list(self._scope_tree.local_scopes),
|
|
583
|
-
resolved_resource_sources=resolved_res_sources,
|
|
584
568
|
variable_definitions=all_var_defs,
|
|
585
569
|
)
|
|
586
570
|
|
|
@@ -612,7 +596,7 @@ class Namespace:
|
|
|
612
596
|
)
|
|
613
597
|
|
|
614
598
|
resolver = ImportResolver(imports_manager, data.source, scope, sentinel=sentinel)
|
|
615
|
-
resolved = resolver.resolve(data.imports
|
|
599
|
+
resolved = resolver.resolve(data.imports)
|
|
616
600
|
|
|
617
601
|
# Add imported variables to scope (needed for variable lookup)
|
|
618
602
|
for resource_entry in resolved.resources.values():
|
|
@@ -637,6 +621,11 @@ class Namespace:
|
|
|
637
621
|
for kw in library_doc.keywords:
|
|
638
622
|
kw_by_id[kw.stable_id] = kw
|
|
639
623
|
|
|
624
|
+
# the init of a variables file, which the import statement of the semantic model refers to
|
|
625
|
+
for variables_entry in resolved.variables_imports.values():
|
|
626
|
+
for kw in variables_entry.library_doc.inits:
|
|
627
|
+
kw_by_id[kw.stable_id] = kw
|
|
628
|
+
|
|
640
629
|
# Variables from scope layers (command_line, own, imported, builtin)
|
|
641
630
|
for var in scope.iter_all():
|
|
642
631
|
var_by_id[var.stable_id] = var
|
|
@@ -685,7 +674,7 @@ class Namespace:
|
|
|
685
674
|
# the other maps add the implicit default libraries and the imports of
|
|
686
675
|
# resource files.
|
|
687
676
|
all_entries: Dict[str, LibraryEntry] = {
|
|
688
|
-
|
|
677
|
+
library_entry_key(entry): entry
|
|
689
678
|
for entry in itertools.chain(
|
|
690
679
|
resolved.libraries.values(),
|
|
691
680
|
resolved.resources.values(),
|
|
@@ -905,4 +894,6 @@ class NamespaceBuilder:
|
|
|
905
894
|
semantic_model=analyzer_result.semantic_model,
|
|
906
895
|
dependency_metas=resolved.dependency_metas,
|
|
907
896
|
failed_imports=resolved.failed_imports,
|
|
897
|
+
environment_set=analyzer_result.environment_set,
|
|
898
|
+
environment_values=analyzer_result.environment_values,
|
|
908
899
|
)
|
|
@@ -61,6 +61,7 @@ from ..utils.variables import (
|
|
|
61
61
|
InvalidVariableError,
|
|
62
62
|
VariableMatcher,
|
|
63
63
|
contains_variable,
|
|
64
|
+
environment_value_hash,
|
|
64
65
|
is_number_literal,
|
|
65
66
|
replace_curdir_in_variable_values,
|
|
66
67
|
search_variable,
|
|
@@ -145,6 +146,8 @@ class NamespaceAnalyzer(Visitor):
|
|
|
145
146
|
self._testcase_tag_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
146
147
|
self._metadata_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
147
148
|
self._testcase_metadata_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
149
|
+
self._environment_set: Dict[str, bool] = {}
|
|
150
|
+
self._environment_values: Dict[str, Optional[str]] = {}
|
|
148
151
|
|
|
149
152
|
# Phase 1+2 results (set by resolve())
|
|
150
153
|
self._library_doc: ResourceDoc = cast(ResourceDoc, None) # set by resolve()
|
|
@@ -245,6 +248,8 @@ class NamespaceAnalyzer(Visitor):
|
|
|
245
248
|
self._metadata_references,
|
|
246
249
|
self._testcase_metadata_references,
|
|
247
250
|
self._scope_builder.build(self._variable_scope),
|
|
251
|
+
environment_set=self._environment_set,
|
|
252
|
+
environment_values=self._environment_values,
|
|
248
253
|
)
|
|
249
254
|
|
|
250
255
|
def _visit_VariableSection(self, node: VariableSection) -> None: # noqa: N802
|
|
@@ -673,7 +678,10 @@ class NamespaceAnalyzer(Visitor):
|
|
|
673
678
|
and cast(EnvironmentVariableDefinition, var).default_value is None
|
|
674
679
|
):
|
|
675
680
|
env_name = var.name[2:-1]
|
|
676
|
-
|
|
681
|
+
# the namespace cache compares every read of the environment when it restores the analysis
|
|
682
|
+
env_value = os.environ.get(env_name, None)
|
|
683
|
+
self._environment_set[env_name] = env_value is not None
|
|
684
|
+
if env_value is None:
|
|
677
685
|
self._append_diagnostics(
|
|
678
686
|
range=range_from_token(var_token),
|
|
679
687
|
message=(
|
|
@@ -2039,7 +2047,9 @@ class NamespaceAnalyzer(Visitor):
|
|
|
2039
2047
|
return name_default
|
|
2040
2048
|
return False
|
|
2041
2049
|
env_name, sep, default = env_inner.partition("=")
|
|
2050
|
+
# the namespace cache compares every read of the environment when it restores the analysis
|
|
2042
2051
|
env_val = os.environ.get(env_name)
|
|
2052
|
+
self._environment_values[env_name] = environment_value_hash(env_val)
|
|
2043
2053
|
if env_val is not None:
|
|
2044
2054
|
return env_val
|
|
2045
2055
|
if sep:
|
|
@@ -98,6 +98,7 @@ from ...utils.variables import (
|
|
|
98
98
|
InvalidVariableError,
|
|
99
99
|
VariableMatcher,
|
|
100
100
|
contains_variable,
|
|
101
|
+
environment_value_hash,
|
|
101
102
|
is_number_literal,
|
|
102
103
|
replace_curdir_in_variable_values,
|
|
103
104
|
search_variable,
|
|
@@ -280,6 +281,8 @@ class SemanticAnalyzer(Visitor):
|
|
|
280
281
|
self._testcase_tag_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
281
282
|
self._metadata_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
282
283
|
self._testcase_metadata_references: Dict[str, Set[Location]] = defaultdict(set)
|
|
284
|
+
self._environment_set: Dict[str, bool] = {}
|
|
285
|
+
self._environment_values: Dict[str, Optional[str]] = {}
|
|
283
286
|
|
|
284
287
|
# Phase 1+2 results (set by resolve())
|
|
285
288
|
self._library_doc: ResourceDoc = cast(ResourceDoc, None)
|
|
@@ -422,6 +425,8 @@ class SemanticAnalyzer(Visitor):
|
|
|
422
425
|
self._metadata_references,
|
|
423
426
|
self._testcase_metadata_references,
|
|
424
427
|
self._scope_builder.build(self._variable_scope),
|
|
428
|
+
environment_set=self._environment_set,
|
|
429
|
+
environment_values=self._environment_values,
|
|
425
430
|
semantic_model=self._semantic_model,
|
|
426
431
|
)
|
|
427
432
|
|
|
@@ -2004,7 +2009,10 @@ class SemanticAnalyzer(Visitor):
|
|
|
2004
2009
|
and cast(EnvironmentVariableDefinition, var).default_value is None
|
|
2005
2010
|
):
|
|
2006
2011
|
env_name = var.name[2:-1]
|
|
2007
|
-
|
|
2012
|
+
# the namespace cache compares every read of the environment when it restores the analysis
|
|
2013
|
+
env_value = os.environ.get(env_name, None)
|
|
2014
|
+
self._environment_set[env_name] = env_value is not None
|
|
2015
|
+
if env_value is None:
|
|
2008
2016
|
self._append_diagnostics(
|
|
2009
2017
|
range=range_from_token(var_token),
|
|
2010
2018
|
message=(
|
|
@@ -4456,7 +4464,9 @@ class SemanticAnalyzer(Visitor):
|
|
|
4456
4464
|
return name_default
|
|
4457
4465
|
return False
|
|
4458
4466
|
env_name, sep, default = env_inner.partition("=")
|
|
4467
|
+
# the namespace cache compares every read of the environment when it restores the analysis
|
|
4459
4468
|
env_val = os.environ.get(env_name)
|
|
4469
|
+
self._environment_values[env_name] = environment_value_hash(env_val)
|
|
4460
4470
|
if env_val is not None:
|
|
4461
4471
|
return env_val
|
|
4462
4472
|
if sep:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Optional
|
|
3
3
|
|
|
4
|
-
from .enums import NodeKind
|
|
4
|
+
from .enums import NodeKind, TokenKind
|
|
5
5
|
from .nodes import (
|
|
6
6
|
DefinitionBlock,
|
|
7
7
|
DefinitionStatement,
|
|
8
|
+
KeywordCallStatement,
|
|
9
|
+
RunKeywordCallStatement,
|
|
8
10
|
SemanticBlock,
|
|
9
11
|
SemanticNode,
|
|
10
12
|
SemanticStatement,
|
|
@@ -23,6 +25,13 @@ _SECTION_KINDS: FrozenSet[NodeKind] = frozenset(
|
|
|
23
25
|
}
|
|
24
26
|
)
|
|
25
27
|
|
|
28
|
+
# The namespace and the name of a keyword reference; the span from the first to the
|
|
29
|
+
# last of them also covers the "." between. An inner call of a Run Keyword variant
|
|
30
|
+
# carries its name as KEYWORD_INNER.
|
|
31
|
+
_KEYWORD_REFERENCE_KINDS: FrozenSet[TokenKind] = frozenset(
|
|
32
|
+
{TokenKind.NAMESPACE, TokenKind.KEYWORD, TokenKind.KEYWORD_INNER}
|
|
33
|
+
)
|
|
34
|
+
|
|
26
35
|
if TYPE_CHECKING:
|
|
27
36
|
from ...utils.variables import VariableMatcher
|
|
28
37
|
from ..entities import VariableDefinition
|
|
@@ -193,6 +202,37 @@ class SemanticModel:
|
|
|
193
202
|
SemanticModel._collect_token_path(sub, line, col, path)
|
|
194
203
|
return
|
|
195
204
|
|
|
205
|
+
def keyword_call_at(self, line: int, col: int) -> Optional[KeywordCallStatement]:
|
|
206
|
+
"""Get the keyword call whose keyword reference covers a position.
|
|
207
|
+
|
|
208
|
+
The keyword reference is the namespace and the keyword name with the
|
|
209
|
+
"." between them, without a BDD prefix; its end counts as covered, as
|
|
210
|
+
in the legacy check of a keyword token. Besides the call that
|
|
211
|
+
`statement_at()` returns, this finds the calls in the body of an
|
|
212
|
+
inline IF, which are statements of their own on the line, and the
|
|
213
|
+
inner calls of a Run Keyword variant, searched recursively.
|
|
214
|
+
O(n) in statements.
|
|
215
|
+
"""
|
|
216
|
+
for stmt in self.statements:
|
|
217
|
+
if isinstance(stmt, KeywordCallStatement) and stmt.line_start <= line <= stmt.line_end:
|
|
218
|
+
call = self._keyword_call_covering(stmt, line, col)
|
|
219
|
+
if call is not None:
|
|
220
|
+
return call
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _keyword_call_covering(stmt: KeywordCallStatement, line: int, col: int) -> Optional[KeywordCallStatement]:
|
|
225
|
+
reference = [token for token in stmt.tokens if token.kind in _KEYWORD_REFERENCE_KINDS]
|
|
226
|
+
if reference and reference[0].line == line:
|
|
227
|
+
if reference[0].col_offset <= col <= reference[-1].col_offset + reference[-1].length:
|
|
228
|
+
return stmt
|
|
229
|
+
if isinstance(stmt, RunKeywordCallStatement):
|
|
230
|
+
for inner in stmt.inner_calls:
|
|
231
|
+
call = SemanticModel._keyword_call_covering(inner, line, col)
|
|
232
|
+
if call is not None:
|
|
233
|
+
return call
|
|
234
|
+
return None
|
|
235
|
+
|
|
196
236
|
def enclosing_definition(self, line: int) -> Optional["DefinitionBlock | DefinitionStatement"]:
|
|
197
237
|
"""Find the enclosing definition (Keyword/TestCase) containing the given line.
|
|
198
238
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import TYPE_CHECKING, FrozenSet, List, Optional, Tuple
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Tuple
|
|
3
3
|
|
|
4
4
|
from robotcode.core.lsp.types import Position, Range
|
|
5
5
|
|
|
6
|
+
from ..entities import library_entry_key
|
|
6
7
|
from .enums import (
|
|
7
8
|
ForFlavor,
|
|
8
9
|
ForZipMode,
|
|
@@ -19,6 +20,16 @@ if TYPE_CHECKING:
|
|
|
19
20
|
from ..library_doc import ArgumentSpec, KeywordDoc
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _slot_values(node: object) -> Dict[str, Any]:
|
|
24
|
+
"""The values of all slots of a node, the base of a statement's pickled state."""
|
|
25
|
+
return {slot: getattr(node, slot) for cls in type(node).__mro__ for slot in getattr(cls, "__slots__", ())}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _set_slot_values(node: object, state: Dict[str, Any]) -> None:
|
|
29
|
+
for slot, value in state.items():
|
|
30
|
+
setattr(node, slot, value)
|
|
31
|
+
|
|
32
|
+
|
|
22
33
|
@dataclass(slots=True)
|
|
23
34
|
class SemanticToken:
|
|
24
35
|
"""A single resolved token in the Semantic Model.
|
|
@@ -136,6 +147,21 @@ class KeywordCallStatement(SemanticStatement):
|
|
|
136
147
|
# Variables that receive the return value: ${result}= My Keyword
|
|
137
148
|
assign_variables: List[SemanticToken] = field(default_factory=list)
|
|
138
149
|
|
|
150
|
+
def __getstate__(self) -> Dict[str, Any]:
|
|
151
|
+
# The namespace disk cache stores ids for the keyword and the library
|
|
152
|
+
# entry, which belong to the analysis of the imports;
|
|
153
|
+
# `resolve_references()` maps them back to the restored namespace's
|
|
154
|
+
# objects. Inner calls replace their own references.
|
|
155
|
+
state = _slot_values(self)
|
|
156
|
+
if self.keyword_doc is not None:
|
|
157
|
+
state["keyword_doc"] = self.keyword_doc.stable_id
|
|
158
|
+
if self.lib_entry is not None:
|
|
159
|
+
state["lib_entry"] = library_entry_key(self.lib_entry)
|
|
160
|
+
return state
|
|
161
|
+
|
|
162
|
+
def __setstate__(self, state: Dict[str, Any]) -> None:
|
|
163
|
+
_set_slot_values(self, state)
|
|
164
|
+
|
|
139
165
|
|
|
140
166
|
@dataclass(slots=True)
|
|
141
167
|
class RunKeywordCallStatement(KeywordCallStatement):
|
|
@@ -274,6 +300,18 @@ class ImportStatement(SemanticStatement):
|
|
|
274
300
|
# second AST walk + libdoc lookup.
|
|
275
301
|
init_keyword_doc: Optional["KeywordDoc"] = None
|
|
276
302
|
|
|
303
|
+
def __getstate__(self) -> Dict[str, Any]:
|
|
304
|
+
# ids in the namespace disk cache, see `KeywordCallStatement.__getstate__`
|
|
305
|
+
state = _slot_values(self)
|
|
306
|
+
if self.lib_entry is not None:
|
|
307
|
+
state["lib_entry"] = library_entry_key(self.lib_entry)
|
|
308
|
+
if self.init_keyword_doc is not None:
|
|
309
|
+
state["init_keyword_doc"] = self.init_keyword_doc.stable_id
|
|
310
|
+
return state
|
|
311
|
+
|
|
312
|
+
def __setstate__(self, state: Dict[str, Any]) -> None:
|
|
313
|
+
_set_slot_values(self, state)
|
|
314
|
+
|
|
277
315
|
|
|
278
316
|
# --- Settings ---
|
|
279
317
|
|
|
@@ -328,6 +366,16 @@ class TemplateDataStatement(SemanticStatement):
|
|
|
328
366
|
|
|
329
367
|
template_keyword_doc: Optional["KeywordDoc"] = None
|
|
330
368
|
|
|
369
|
+
def __getstate__(self) -> Dict[str, Any]:
|
|
370
|
+
# an id in the namespace disk cache, see `KeywordCallStatement.__getstate__`
|
|
371
|
+
state = _slot_values(self)
|
|
372
|
+
if self.template_keyword_doc is not None:
|
|
373
|
+
state["template_keyword_doc"] = self.template_keyword_doc.stable_id
|
|
374
|
+
return state
|
|
375
|
+
|
|
376
|
+
def __setstate__(self, state: Dict[str, Any]) -> None:
|
|
377
|
+
_set_slot_values(self, state)
|
|
378
|
+
|
|
331
379
|
|
|
332
380
|
# --- Blocks (structural containers) ---
|
|
333
381
|
|
|
@@ -5,13 +5,14 @@ stable_id strings back to live objects, and pickle helper utilities.
|
|
|
5
5
|
|
|
6
6
|
The SemanticModel is designed to be pickle-friendly:
|
|
7
7
|
- SemanticToken is a pure value object (no live references) - pickles natively
|
|
8
|
-
- SemanticStatement subclasses hold keyword_doc/lib_entry
|
|
9
|
-
are live objects that cannot survive
|
|
8
|
+
- SemanticStatement subclasses hold keyword_doc/lib_entry/init_keyword_doc/
|
|
9
|
+
template_keyword_doc references which are live objects that cannot survive
|
|
10
|
+
pickle/unpickle across sessions
|
|
10
11
|
|
|
11
12
|
For serialization:
|
|
12
|
-
1.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
1. When pickled, the statements store stable_id strings and library entry
|
|
14
|
+
keys instead of these references (`__getstate__` in nodes.py)
|
|
15
|
+
2. After unpickling: call resolve_references() to restore live objects
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from typing import TYPE_CHECKING, Dict, Optional
|
|
@@ -24,6 +25,7 @@ from .nodes import (
|
|
|
24
25
|
RunKeywordCallStatement,
|
|
25
26
|
SemanticBlock,
|
|
26
27
|
SemanticStatement,
|
|
28
|
+
TemplateDataStatement,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
if TYPE_CHECKING:
|
|
@@ -39,10 +41,11 @@ def resolve_references(
|
|
|
39
41
|
) -> None:
|
|
40
42
|
"""Resolve stable_id strings back to live objects after unpickling.
|
|
41
43
|
|
|
42
|
-
After unpickling a SemanticModel, keyword_doc
|
|
43
|
-
|
|
44
|
+
After unpickling a SemanticModel, the keyword_doc, lib_entry,
|
|
45
|
+
init_keyword_doc and template_keyword_doc fields contain stable_id
|
|
46
|
+
strings and library entry keys instead of live objects. This function
|
|
44
47
|
walks the model and replaces them with the actual objects from the
|
|
45
|
-
provided lookup dicts.
|
|
48
|
+
provided lookup dicts; an id that is not in them resolves to None.
|
|
46
49
|
|
|
47
50
|
Args:
|
|
48
51
|
model: The unpickled SemanticModel to resolve.
|
|
@@ -85,6 +88,9 @@ def _resolve_statement(
|
|
|
85
88
|
_resolve_keyword_call(stmt, kw_by_id, entry_by_key)
|
|
86
89
|
elif isinstance(stmt, ImportStatement):
|
|
87
90
|
stmt.lib_entry = _resolve_entry(stmt.lib_entry, entry_by_key)
|
|
91
|
+
stmt.init_keyword_doc = _resolve_keyword(stmt.init_keyword_doc, kw_by_id)
|
|
92
|
+
elif isinstance(stmt, TemplateDataStatement):
|
|
93
|
+
stmt.template_keyword_doc = _resolve_keyword(stmt.template_keyword_doc, kw_by_id)
|
|
88
94
|
elif isinstance(stmt, DefinitionStatement):
|
|
89
95
|
if isinstance(stmt.arguments_spec, str):
|
|
90
96
|
# arguments_spec should not be serialized as string,
|
|
@@ -98,9 +104,7 @@ def _resolve_keyword_call(
|
|
|
98
104
|
entry_by_key: Dict[str, "LibraryEntry"],
|
|
99
105
|
) -> None:
|
|
100
106
|
"""Resolve references in a keyword call statement."""
|
|
101
|
-
|
|
102
|
-
stmt.keyword_doc = kw_by_id.get(stmt.keyword_doc) # type: ignore[unreachable]
|
|
103
|
-
|
|
107
|
+
stmt.keyword_doc = _resolve_keyword(stmt.keyword_doc, kw_by_id)
|
|
104
108
|
stmt.lib_entry = _resolve_entry(stmt.lib_entry, entry_by_key)
|
|
105
109
|
|
|
106
110
|
if isinstance(stmt, RunKeywordCallStatement):
|
|
@@ -108,6 +112,16 @@ def _resolve_keyword_call(
|
|
|
108
112
|
_resolve_keyword_call(inner, kw_by_id, entry_by_key)
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
def _resolve_keyword(
|
|
116
|
+
keyword_doc: Optional["KeywordDoc"],
|
|
117
|
+
kw_by_id: Dict[str, "KeywordDoc"],
|
|
118
|
+
) -> Optional["KeywordDoc"]:
|
|
119
|
+
"""Resolve a single keyword reference."""
|
|
120
|
+
if isinstance(keyword_doc, str):
|
|
121
|
+
return kw_by_id.get(keyword_doc) # type: ignore[unreachable]
|
|
122
|
+
return keyword_doc
|
|
123
|
+
|
|
124
|
+
|
|
111
125
|
def _resolve_entry(
|
|
112
126
|
entry: Optional["LibraryEntry"],
|
|
113
127
|
entry_by_key: Dict[str, "LibraryEntry"],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import hashlib
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Any, Optional, Sequence, Tuple, cast
|
|
4
5
|
|
|
@@ -275,3 +276,11 @@ def replace_curdir_in_variable_values(values: Sequence[str], source: str) -> Tup
|
|
|
275
276
|
"""Replace ${CURDIR} in variable values with the escaped parent directory of source."""
|
|
276
277
|
curdir = str(Path(source).parent).replace("\\", "\\\\")
|
|
277
278
|
return tuple(s.replace("${CURDIR}", curdir) for s in values)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def environment_value_hash(value: Optional[str]) -> Optional[str]:
|
|
282
|
+
"""The SHA-256 of the value of an environment variable, or None when it is not set.
|
|
283
|
+
|
|
284
|
+
What the analysis cache keeps instead of the value, so that no value of the environment ends up in it.
|
|
285
|
+
"""
|
|
286
|
+
return hashlib.sha256(value.encode("utf-8", "surrogateescape")).hexdigest() if value is not None else None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: robotcode-robot
|
|
3
|
-
Version: 2.8.0-dev.
|
|
3
|
+
Version: 2.8.0-dev.677
|
|
4
4
|
Summary: Support classes for RobotCode for handling Robot Framework projects.
|
|
5
5
|
Project-URL: Homepage, https://robotcode.io
|
|
6
6
|
Project-URL: Donate, https://opencollective.com/robotcode
|
|
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
|
|
|
27
27
|
Classifier: Typing :: Typed
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
29
|
Requires-Dist: platformdirs>=4.3
|
|
30
|
-
Requires-Dist: robotcode-core==2.8.0-dev.
|
|
30
|
+
Requires-Dist: robotcode-core==2.8.0-dev.677
|
|
31
31
|
Requires-Dist: robotframework>=5.0.0
|
|
32
32
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
robotcode/robot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
robotcode/robot/__version__.py,sha256=
|
|
2
|
+
robotcode/robot/__version__.py,sha256=0xyLlet74gxp40f05fA-fjXhAr2XdtEik3UuIJs6yAM,30
|
|
3
3
|
robotcode/robot/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
|
|
4
4
|
robotcode/robot/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
robotcode/robot/config/loader.py,sha256=HLbkZem8JiBuy9Q5naghZddAuMPOn8Tk6UBgN2T3Uyc,9073
|
|
6
|
-
robotcode/robot/config/model.py,sha256=
|
|
6
|
+
robotcode/robot/config/model.py,sha256=i8EH_n_20U7eCNY81nWa7oTFAxKj9iSH5Sxm1vr8bOQ,112151
|
|
7
7
|
robotcode/robot/config/utils.py,sha256=A8gD5IItxuC7AY5Olcc8tmsSbqCCi_vZPVoK3BqWt_8,4070
|
|
8
8
|
robotcode/robot/diagnostics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
robotcode/robot/diagnostics/analyzer_result.py,sha256=
|
|
9
|
+
robotcode/robot/diagnostics/analyzer_result.py,sha256=5pHW5Yf3C3rmaDvmM7tP0y_qwqLW3YHhkeVV-qDhxtg,1459
|
|
10
10
|
robotcode/robot/diagnostics/data_cache.py,sha256=Yi52oQTUBFgcPK0UF3lDAj4mRFqoD_VOtrAUyRWXnrQ,18200
|
|
11
11
|
robotcode/robot/diagnostics/diagnostic_rules.py,sha256=7b4vbGTEuxCef8AHTshV4XAJe75aR4-lJ5QEDyuZtcE,3558
|
|
12
12
|
robotcode/robot/diagnostics/diagnostics_modifier.py,sha256=MftxJlUEDsn9ZiOsBDApXC2N-4OjXHSZj1Y9PXXlSAg,11742
|
|
13
|
-
robotcode/robot/diagnostics/document_cache_helper.py,sha256=
|
|
14
|
-
robotcode/robot/diagnostics/entities.py,sha256=
|
|
13
|
+
robotcode/robot/diagnostics/document_cache_helper.py,sha256=llwyocHMKrSpuooXMuBalwnG6hlY6YgMTIPFq2tHFr8,35462
|
|
14
|
+
robotcode/robot/diagnostics/entities.py,sha256=vE5szceIL-ANqJPnAbqIebg3kLddE9OPhxwBL9iKP-M,11949
|
|
15
15
|
robotcode/robot/diagnostics/errors.py,sha256=lqHDWrMNy3ZhuEIVxeEhaU0ns8PB6pVTQBfaMqRQ8L4,2733
|
|
16
|
-
robotcode/robot/diagnostics/import_resolver.py,sha256=
|
|
17
|
-
robotcode/robot/diagnostics/imports_manager.py,sha256=
|
|
16
|
+
robotcode/robot/diagnostics/import_resolver.py,sha256=DZjUOD3uNxFAwsVUqrCTRhColjz9DRPetAvIb_Ccufs,32588
|
|
17
|
+
robotcode/robot/diagnostics/imports_manager.py,sha256=opemApuCLgPNsnqVuYc9MefqHt42g_7Ozqhrmu_osH0,90401
|
|
18
18
|
robotcode/robot/diagnostics/keyword_finder.py,sha256=nwTmjvhyu5H7HHzyk8gt-pny2K2xdH2qCNyKXwwXRnk,18998
|
|
19
19
|
robotcode/robot/diagnostics/library_doc.py,sha256=MYztjgnOmXAPjXn7td651oWats2qDlPrLgCkmwpdSmU,153576
|
|
20
20
|
robotcode/robot/diagnostics/model_helper.py,sha256=_ySXwerkK5xvjWa82T0DFy8jpxRzcaavTJiAHnk2lXQ,29740
|
|
21
|
-
robotcode/robot/diagnostics/namespace.py,sha256=
|
|
22
|
-
robotcode/robot/diagnostics/namespace_analyzer.py,sha256=
|
|
21
|
+
robotcode/robot/diagnostics/namespace.py,sha256=Rcmjk3V0vcY-DUkgjkRyuOdYXurKaTEqXJLc6efYrwY,35489
|
|
22
|
+
robotcode/robot/diagnostics/namespace_analyzer.py,sha256=dGAEbbznPJlFBKN4dLkMDU7w9xc0mYdmRARvRS3Zlwg,104331
|
|
23
23
|
robotcode/robot/diagnostics/project_index.py,sha256=Oqs7kV279PixVS6rGeigPKXGJYGqJdTDmDBjU7ODDRA,11006
|
|
24
24
|
robotcode/robot/diagnostics/scope_tree.py,sha256=UREHoFG7NWh6UTWxET1M5Jxz_XBjXaQJg1GUIYPctq0,8348
|
|
25
25
|
robotcode/robot/diagnostics/variable_scope.py,sha256=bge49DM-1FfQYIeqK2DtBG7JAMm25mazZ_zjh0DoMmg,3096
|
|
26
26
|
robotcode/robot/diagnostics/workspace_config.py,sha256=GlsR1VnUBHoLBvIZtUXkm1dBDFPT9qoiYZzwZlurmXM,3277
|
|
27
27
|
robotcode/robot/diagnostics/semantic_analyzer/__init__.py,sha256=ObbJv-S8LDgkWKZtTZjISBodXxyoLdEiOjVYp5aYbT4,1689
|
|
28
|
-
robotcode/robot/diagnostics/semantic_analyzer/analyzer.py,sha256=
|
|
28
|
+
robotcode/robot/diagnostics/semantic_analyzer/analyzer.py,sha256=CkROVL_B3QsSULXkHkRDTddLCR0sswVVxEKCQUYk_qs,206149
|
|
29
29
|
robotcode/robot/diagnostics/semantic_analyzer/enums.py,sha256=FIf3bHHqYY2etzpNrZTpZxGFsdtBE15lTssI1g8T2ro,8668
|
|
30
30
|
robotcode/robot/diagnostics/semantic_analyzer/json_dump.py,sha256=2Yq3CLAKF5IEqKeFDoIYn-Ize_1Ucgz82ORkx3wAguY,13836
|
|
31
|
-
robotcode/robot/diagnostics/semantic_analyzer/model.py,sha256=
|
|
32
|
-
robotcode/robot/diagnostics/semantic_analyzer/nodes.py,sha256=
|
|
31
|
+
robotcode/robot/diagnostics/semantic_analyzer/model.py,sha256=TTsrU_aDSC4JwbdypxSaGR79iCN071Y5VUMO8vGm5IE,20476
|
|
32
|
+
robotcode/robot/diagnostics/semantic_analyzer/nodes.py,sha256=z3EM_e7AOQ6r6Ya0iS9e4wpDXDMg5Hk-WljoWBUDOWE,18433
|
|
33
33
|
robotcode/robot/diagnostics/semantic_analyzer/run_keyword.py,sha256=tfMaq982MaYlwcJViPa6w7eNuruFz1H_TWjrbefsMjA,1876
|
|
34
|
-
robotcode/robot/diagnostics/semantic_analyzer/serialization.py,sha256=
|
|
34
|
+
robotcode/robot/diagnostics/semantic_analyzer/serialization.py,sha256=DA-_gRbo_9D6BbCMeIg6z2_8sNBY6uayOu9y4xhjK7Q,4896
|
|
35
35
|
robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py,sha256=vBor3GZjvZX33I9yQliUtHZbjDybWn7W0YoSd5EMO_M,33646
|
|
36
36
|
robotcode/robot/utils/__init__.py,sha256=-A0YeHv7e7WK1SId7Er7Vu6frrDYWopUrcyFlyxLmPY,396
|
|
37
37
|
robotcode/robot/utils/ast.py,sha256=S9xyiJZee4Y5VcroNrGIIWp8f9sutb_1o4blAlCqzdg,11218
|
|
@@ -41,8 +41,8 @@ robotcode/robot/utils/match.py,sha256=KgA6o6TbnQpOT1eAzEjhl_-FZ2XO07xiV0L7IYRzyz
|
|
|
41
41
|
robotcode/robot/utils/robot_patching.py,sha256=jXHhnPvZx8hCQq4B_2uwX9j51Be_toWpUoTYYe8HjPo,1463
|
|
42
42
|
robotcode/robot/utils/robot_path.py,sha256=ObLP8PqamTZAz5oi0eDXE_dnyZwAPXp4TJYm6ugeENs,2185
|
|
43
43
|
robotcode/robot/utils/stubs.py,sha256=umugZYAyneFNgqRJBRMJPzm0u0B_TH8Sx_y-ykXnxpw,351
|
|
44
|
-
robotcode/robot/utils/variables.py,sha256=
|
|
44
|
+
robotcode/robot/utils/variables.py,sha256=9WnP3QqcHdq-zioJ_gJXJobw_2Gcto_rGPCUGmOH9YQ,9829
|
|
45
45
|
robotcode/robot/utils/visitor.py,sha256=nP3O0qh3YYuxR6S8wYJRBFfNwIVgsgohURBlrnFkRYQ,2299
|
|
46
|
-
robotcode_robot-2.8.0.
|
|
47
|
-
robotcode_robot-2.8.0.
|
|
48
|
-
robotcode_robot-2.8.0.
|
|
46
|
+
robotcode_robot-2.8.0.dev677.dist-info/METADATA,sha256=-WMG4AXbck6ZGANXnHl578n_RxVj5Y5o7iron1Hu8OU,2960
|
|
47
|
+
robotcode_robot-2.8.0.dev677.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
48
|
+
robotcode_robot-2.8.0.dev677.dist-info/RECORD,,
|
|
File without changes
|