robotcode-robot 2.6.2__py3-none-any.whl → 2.8.0.dev548__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/loader.py +18 -5
- robotcode/robot/config/model.py +258 -61
- robotcode/robot/diagnostics/analyzer_result.py +1 -0
- robotcode/robot/diagnostics/data_cache.py +12 -1
- robotcode/robot/diagnostics/diagnostic_rules.py +75 -0
- robotcode/robot/diagnostics/diagnostics_modifier.py +83 -43
- robotcode/robot/diagnostics/document_cache_helper.py +158 -17
- robotcode/robot/diagnostics/errors.py +7 -0
- robotcode/robot/diagnostics/import_resolver.py +178 -12
- robotcode/robot/diagnostics/imports_manager.py +556 -220
- robotcode/robot/diagnostics/library_doc.py +874 -162
- robotcode/robot/diagnostics/namespace.py +84 -18
- robotcode/robot/diagnostics/namespace_analyzer.py +59 -16
- robotcode/robot/diagnostics/project_index.py +88 -13
- robotcode/robot/diagnostics/scope_tree.py +2 -2
- robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +790 -201
- robotcode/robot/diagnostics/semantic_analyzer/enums.py +39 -2
- robotcode/robot/diagnostics/semantic_analyzer/json_dump.py +364 -0
- robotcode/robot/diagnostics/semantic_analyzer/model.py +72 -11
- robotcode/robot/diagnostics/semantic_analyzer/nodes.py +7 -1
- robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py +218 -112
- robotcode/robot/diagnostics/workspace_config.py +3 -0
- robotcode/robot/utils/markdown_docs.py +733 -0
- robotcode/robot/utils/markdownformatter.py +57 -12
- robotcode/robot/utils/match.py +13 -0
- robotcode/robot/utils/robot_patching.py +4 -0
- robotcode/robot/utils/variables.py +3 -0
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/METADATA +4 -3
- robotcode_robot-2.8.0.dev548.dist-info/RECORD +48 -0
- {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/WHEEL +1 -1
- robotcode_robot-2.6.2.dist-info/RECORD +0 -46
|
@@ -12,7 +12,7 @@ containing all resolved entries and collected diagnostics.
|
|
|
12
12
|
import os
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
15
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|
16
16
|
|
|
17
17
|
from robotcode.core.lsp.types import (
|
|
18
18
|
Diagnostic,
|
|
@@ -26,6 +26,7 @@ from robotcode.core.uri import Uri
|
|
|
26
26
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
27
27
|
from robotcode.core.utils.path import file_id, same_file_id
|
|
28
28
|
|
|
29
|
+
from ..utils import RF_VERSION
|
|
29
30
|
from .entities import (
|
|
30
31
|
Import,
|
|
31
32
|
LibraryEntry,
|
|
@@ -36,15 +37,44 @@ from .entities import (
|
|
|
36
37
|
VariablesImport,
|
|
37
38
|
)
|
|
38
39
|
from .errors import DIAGNOSTICS_SOURCE_NAME, Error
|
|
39
|
-
from .imports_manager import ImportsManager
|
|
40
|
-
from .library_doc import BUILTIN_LIBRARY_NAME, DEFAULT_LIBRARIES
|
|
40
|
+
from .imports_manager import ImportsManager, LibraryMetaData
|
|
41
|
+
from .library_doc import BUILTIN_LIBRARY_NAME, DEFAULT_LIBRARIES, resolve_args
|
|
41
42
|
from .variable_scope import VariableScope
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _dependency_key(kind: str, meta: Optional[Any], import_name: str) -> str:
|
|
46
|
+
"""The key of a library or variable-file dependency of the namespace cache.
|
|
47
|
+
|
|
48
|
+
It names the file of the meta, so that two files imported with the same
|
|
49
|
+
name from different folders are dependencies of their own. Without a meta
|
|
50
|
+
the namespace is not cached anyway, and the import name is enough.
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(meta, LibraryMetaData):
|
|
53
|
+
try:
|
|
54
|
+
return f"{kind}:{meta.cache_key}"
|
|
55
|
+
except ValueError:
|
|
56
|
+
pass
|
|
57
|
+
return f"{kind}:{import_name}"
|
|
58
|
+
|
|
59
|
+
|
|
44
60
|
class _NameSpaceError(Exception):
|
|
45
61
|
pass
|
|
46
62
|
|
|
47
63
|
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class FailedImport:
|
|
66
|
+
"""An import whose resolution raised or whose library or variables documentation has errors.
|
|
67
|
+
|
|
68
|
+
`base_dir` and `variables` are what its file or module was looked up with.
|
|
69
|
+
`found` tells whether that lookup succeeded, so that only loading failed.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
imp: Import
|
|
73
|
+
base_dir: str
|
|
74
|
+
variables: Dict[str, Any]
|
|
75
|
+
found: bool
|
|
76
|
+
|
|
77
|
+
|
|
48
78
|
@dataclass(frozen=True, slots=True)
|
|
49
79
|
class ResolvedImports:
|
|
50
80
|
"""Result of import resolution (Phase 2).
|
|
@@ -59,6 +89,16 @@ class ResolvedImports:
|
|
|
59
89
|
resources: Dict[str, ResourceEntry] = field(default_factory=dict)
|
|
60
90
|
variables_imports: Dict[str, VariablesEntry] = field(default_factory=dict)
|
|
61
91
|
|
|
92
|
+
# Metadata of each resolved dependency, captured together with its doc at
|
|
93
|
+
# resolve time (keys: "lib:{cache_key}", "res:{source}", "var:{cache_key}", see
|
|
94
|
+
# _dependency_key; the import name when there is no meta).
|
|
95
|
+
# A None value means the dependency was resolved but no trustworthy meta
|
|
96
|
+
# exists — namespaces built from it must not be persisted.
|
|
97
|
+
dependency_metas: Dict[str, Optional[Any]] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
# The imports that failed, also those of imported resource files.
|
|
100
|
+
failed_imports: List[FailedImport] = field(default_factory=list)
|
|
101
|
+
|
|
62
102
|
|
|
63
103
|
class ImportResolver:
|
|
64
104
|
"""Resolves all imports for a Robot Framework file.
|
|
@@ -78,10 +118,13 @@ class ImportResolver:
|
|
|
78
118
|
|
|
79
119
|
__slots__ = (
|
|
80
120
|
"_base_dir",
|
|
121
|
+
"_dependency_metas",
|
|
81
122
|
"_diagnostics",
|
|
123
|
+
"_failed_imports",
|
|
82
124
|
"_import_entries",
|
|
83
125
|
"_imports_manager",
|
|
84
126
|
"_libraries",
|
|
127
|
+
"_library_args",
|
|
85
128
|
"_resources",
|
|
86
129
|
"_scope",
|
|
87
130
|
"_sentinel",
|
|
@@ -108,6 +151,8 @@ class ImportResolver:
|
|
|
108
151
|
self._base_dir = str(Path(source).parent)
|
|
109
152
|
|
|
110
153
|
self._libraries: Dict[str, LibraryEntry] = {}
|
|
154
|
+
# the arguments of each entry in _libraries, resolved when it was imported
|
|
155
|
+
self._library_args: Dict[str, Tuple[Any, ...]] = {}
|
|
111
156
|
self._resources: Dict[str, ResourceEntry] = {}
|
|
112
157
|
self._variables_imports: Dict[str, VariablesEntry] = {}
|
|
113
158
|
self._import_entries: Dict[Import, LibraryEntry] = {}
|
|
@@ -115,6 +160,8 @@ class ImportResolver:
|
|
|
115
160
|
self._variables: Dict[str, Any] = {}
|
|
116
161
|
self._variables_dirty = True
|
|
117
162
|
self._source_hints: Dict[str, str] = {}
|
|
163
|
+
self._dependency_metas: Dict[str, Optional[Any]] = {}
|
|
164
|
+
self._failed_imports: List[FailedImport] = []
|
|
118
165
|
|
|
119
166
|
def resolve(self, imports: Iterable[Import], *, source_hints: Optional[Dict[str, str]] = None) -> ResolvedImports:
|
|
120
167
|
"""Resolve all imports and return the result."""
|
|
@@ -129,6 +176,8 @@ class ImportResolver:
|
|
|
129
176
|
variables_imports=self._variables_imports,
|
|
130
177
|
import_entries=self._import_entries,
|
|
131
178
|
diagnostics=self._diagnostics,
|
|
179
|
+
dependency_metas=self._dependency_metas,
|
|
180
|
+
failed_imports=self._failed_imports,
|
|
132
181
|
)
|
|
133
182
|
|
|
134
183
|
# ------------------------------------------------------------------
|
|
@@ -140,6 +189,11 @@ class ImportResolver:
|
|
|
140
189
|
self._variables = self._scope.as_robot_variables()
|
|
141
190
|
self._variables_dirty = False
|
|
142
191
|
|
|
192
|
+
def _record_failed_import(self, imp: Import, base_dir: str) -> None:
|
|
193
|
+
# _refresh_variables replaces the dict, so the import keeps the variables it was resolved with
|
|
194
|
+
found = self._imports_manager.find_import_source(imp, base_dir, self._variables) is not None
|
|
195
|
+
self._failed_imports.append(FailedImport(imp, base_dir, self._variables, found))
|
|
196
|
+
|
|
143
197
|
def _append_diagnostics(
|
|
144
198
|
self,
|
|
145
199
|
range: Range,
|
|
@@ -186,13 +240,14 @@ class ImportResolver:
|
|
|
186
240
|
):
|
|
187
241
|
for library in DEFAULT_LIBRARIES:
|
|
188
242
|
try:
|
|
189
|
-
library_doc = self._imports_manager.
|
|
243
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_library_import_with_meta(
|
|
190
244
|
library,
|
|
191
245
|
(),
|
|
192
246
|
base_dir=self._base_dir,
|
|
193
247
|
sentinel=None,
|
|
194
248
|
variables=self._variables,
|
|
195
249
|
)
|
|
250
|
+
self._dependency_metas[_dependency_key("lib", meta, library)] = meta
|
|
196
251
|
entry = LibraryEntry(
|
|
197
252
|
name=library_doc.name,
|
|
198
253
|
import_name=library,
|
|
@@ -201,6 +256,16 @@ class ImportResolver:
|
|
|
201
256
|
alias=None,
|
|
202
257
|
)
|
|
203
258
|
self._libraries[entry.alias or entry.name or entry.import_name] = entry
|
|
259
|
+
self._library_args[entry.alias or entry.name or entry.import_name] = ()
|
|
260
|
+
|
|
261
|
+
for err in library_doc.errors or []:
|
|
262
|
+
self._append_diagnostics(
|
|
263
|
+
range=Range.zero(),
|
|
264
|
+
message=f"Can't import default library '{library}': {err.message}",
|
|
265
|
+
severity=DiagnosticSeverity.ERROR,
|
|
266
|
+
source="Robot",
|
|
267
|
+
code=err.type_name,
|
|
268
|
+
)
|
|
204
269
|
except (SystemExit, KeyboardInterrupt):
|
|
205
270
|
raise
|
|
206
271
|
except BaseException as e:
|
|
@@ -256,7 +321,7 @@ class ImportResolver:
|
|
|
256
321
|
if isinstance(imp, LibraryImport):
|
|
257
322
|
if imp.name is None:
|
|
258
323
|
raise _NameSpaceError("Library setting requires value.")
|
|
259
|
-
self._import_library(imp, base_dir, top_level=top_level)
|
|
324
|
+
self._import_library(imp, base_dir, top_level=top_level, parent_import=parent_import)
|
|
260
325
|
elif isinstance(imp, ResourceImport):
|
|
261
326
|
if imp.name is None:
|
|
262
327
|
raise _NameSpaceError("Resource setting requires value.")
|
|
@@ -277,6 +342,8 @@ class ImportResolver:
|
|
|
277
342
|
except (SystemExit, KeyboardInterrupt):
|
|
278
343
|
raise
|
|
279
344
|
except BaseException as e:
|
|
345
|
+
self._record_failed_import(imp, base_dir)
|
|
346
|
+
|
|
280
347
|
effective_source = parent_source if parent_source else source
|
|
281
348
|
if top_level:
|
|
282
349
|
self._append_diagnostics(
|
|
@@ -309,16 +376,28 @@ class ImportResolver:
|
|
|
309
376
|
# Type-specific import methods (like RF: _import_library etc.)
|
|
310
377
|
# ------------------------------------------------------------------
|
|
311
378
|
|
|
312
|
-
def
|
|
379
|
+
def _record_dependency_meta(self, key: str, meta: Optional[Any]) -> None:
|
|
380
|
+
# A None meta keeps the namespace out of the namespace cache, so a later
|
|
381
|
+
# import of the same name (other arguments, alias) must not replace it.
|
|
382
|
+
if key in self._dependency_metas and self._dependency_metas[key] is None:
|
|
383
|
+
return
|
|
384
|
+
self._dependency_metas[key] = meta
|
|
385
|
+
|
|
386
|
+
def _import_library(
|
|
387
|
+
self, imp: LibraryImport, base_dir: str, *, top_level: bool, parent_import: Optional[Import] = None
|
|
388
|
+
) -> None:
|
|
313
389
|
assert imp.name is not None
|
|
314
390
|
|
|
315
|
-
library_doc = self._imports_manager.
|
|
391
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_library_import_with_meta(
|
|
316
392
|
imp.name,
|
|
317
393
|
imp.args,
|
|
318
394
|
base_dir=base_dir,
|
|
319
395
|
sentinel=self._sentinel,
|
|
320
396
|
variables=self._variables,
|
|
321
397
|
)
|
|
398
|
+
self._record_dependency_meta(_dependency_key("lib", meta, imp.name), meta)
|
|
399
|
+
if library_doc.errors:
|
|
400
|
+
self._record_failed_import(imp, base_dir)
|
|
322
401
|
entry = LibraryEntry(
|
|
323
402
|
name=library_doc.name,
|
|
324
403
|
import_name=imp.name,
|
|
@@ -358,9 +437,20 @@ class ImportResolver:
|
|
|
358
437
|
),
|
|
359
438
|
None,
|
|
360
439
|
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
440
|
+
key = entry.alias or entry.name or entry.import_name
|
|
441
|
+
args = resolve_args(
|
|
442
|
+
imp.args,
|
|
443
|
+
str(self._imports_manager.root_folder),
|
|
444
|
+
base_dir,
|
|
445
|
+
self._imports_manager.get_resolvable_command_line_variables(),
|
|
446
|
+
self._variables,
|
|
447
|
+
)
|
|
448
|
+
if already_imported is None and key not in self._libraries:
|
|
449
|
+
self._libraries[key] = entry
|
|
450
|
+
self._library_args[key] = args
|
|
451
|
+
elif already_imported is None:
|
|
452
|
+
self._report_ignored_library(imp, entry, key, args, top_level=top_level, parent_import=parent_import)
|
|
453
|
+
elif top_level and already_imported.library_doc.source:
|
|
364
454
|
self._append_diagnostics(
|
|
365
455
|
range=entry.import_range,
|
|
366
456
|
message=f'Library "{entry}" already imported.',
|
|
@@ -388,6 +478,58 @@ class ImportResolver:
|
|
|
388
478
|
if top_level:
|
|
389
479
|
self._report_entry_errors(imp, entry)
|
|
390
480
|
|
|
481
|
+
def _report_ignored_library(
|
|
482
|
+
self,
|
|
483
|
+
imp: LibraryImport,
|
|
484
|
+
entry: LibraryEntry,
|
|
485
|
+
key: str,
|
|
486
|
+
args: Tuple[Any, ...],
|
|
487
|
+
*,
|
|
488
|
+
top_level: bool,
|
|
489
|
+
parent_import: Optional[Import] = None,
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Report a library import that is ignored because another import uses its name.
|
|
492
|
+
|
|
493
|
+
Robot Framework warns about it since 7.4 when the earlier import is
|
|
494
|
+
another library or the same library with other arguments. An import of
|
|
495
|
+
a resource file is reported at the analyzed file's import of it.
|
|
496
|
+
"""
|
|
497
|
+
if RF_VERSION < (7, 4) or not (top_level or (parent_import is not None and imp.source)):
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
earlier = self._libraries[key]
|
|
501
|
+
# Robot Framework reports an import that fails to load as an error and never registers it
|
|
502
|
+
if earlier.library_doc.errors or entry.library_doc.errors:
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
if earlier.name != entry.name:
|
|
506
|
+
reason = f'another library with name "{key}" is already imported'
|
|
507
|
+
elif self._library_args[key] != args:
|
|
508
|
+
reason = f'library "{key}" is already imported with different arguments'
|
|
509
|
+
else:
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
related = self._related_info_from_entry(earlier) or []
|
|
513
|
+
if top_level:
|
|
514
|
+
at = imp.range
|
|
515
|
+
message = f'Library "{imp.name}" is not imported, because {reason}.'
|
|
516
|
+
else:
|
|
517
|
+
assert parent_import is not None
|
|
518
|
+
assert imp.source
|
|
519
|
+
at = parent_import.range
|
|
520
|
+
message = f'Library "{imp.name}" in "{Path(imp.source).name}" is not imported, because {reason}.'
|
|
521
|
+
ignored = Location(uri=str(Uri.from_path(imp.source)), range=imp.range)
|
|
522
|
+
related.insert(0, DiagnosticRelatedInformation(location=ignored, message=""))
|
|
523
|
+
|
|
524
|
+
self._append_diagnostics(
|
|
525
|
+
range=at,
|
|
526
|
+
message=message,
|
|
527
|
+
severity=DiagnosticSeverity.WARNING,
|
|
528
|
+
source=DIAGNOSTICS_SOURCE_NAME,
|
|
529
|
+
related_information=related or None,
|
|
530
|
+
code=Error.LIBRARY_IMPORT_IGNORED,
|
|
531
|
+
)
|
|
532
|
+
|
|
391
533
|
def _import_resource(
|
|
392
534
|
self,
|
|
393
535
|
imp: ResourceImport,
|
|
@@ -407,13 +549,17 @@ class ImportResolver:
|
|
|
407
549
|
if hint is not None and os.path.isfile(hint):
|
|
408
550
|
source_hint = hint
|
|
409
551
|
|
|
410
|
-
resource_doc = self._imports_manager.
|
|
552
|
+
resource_doc, meta = self._imports_manager.get_resource_doc_for_resource_import_with_meta(
|
|
411
553
|
imp.name,
|
|
412
554
|
base_dir,
|
|
413
555
|
sentinel=self._sentinel,
|
|
414
556
|
variables=self._variables,
|
|
415
557
|
source=source_hint,
|
|
416
558
|
)
|
|
559
|
+
if resource_doc.source is not None:
|
|
560
|
+
# setdefault: on re-imports of the same resource the meta captured
|
|
561
|
+
# with the first (analyzed) doc wins.
|
|
562
|
+
self._dependency_metas.setdefault(f"res:{resource_doc.source}", meta)
|
|
417
563
|
entry = ResourceEntry(
|
|
418
564
|
name=resource_doc.name,
|
|
419
565
|
import_name=imp.name,
|
|
@@ -468,6 +614,11 @@ class ImportResolver:
|
|
|
468
614
|
None,
|
|
469
615
|
)
|
|
470
616
|
if already_imported is not None:
|
|
617
|
+
# the statement keeps its own entry, like a repeated library or variables import
|
|
618
|
+
entry.import_range = imp.range
|
|
619
|
+
entry.import_source = imp.source
|
|
620
|
+
self._import_entries[imp] = entry
|
|
621
|
+
|
|
471
622
|
self._logger.debug(lambda: f"Resource '{imp.name}' already imported.", context_name="import")
|
|
472
623
|
if top_level:
|
|
473
624
|
self._append_diagnostics(
|
|
@@ -535,13 +686,16 @@ class ImportResolver:
|
|
|
535
686
|
def _import_variables(self, imp: VariablesImport, base_dir: str, *, top_level: bool) -> None:
|
|
536
687
|
assert imp.name is not None
|
|
537
688
|
|
|
538
|
-
library_doc = self._imports_manager.
|
|
689
|
+
library_doc, meta = self._imports_manager.get_libdoc_for_variables_import_with_meta(
|
|
539
690
|
imp.name,
|
|
540
691
|
imp.args,
|
|
541
692
|
base_dir=base_dir,
|
|
542
693
|
sentinel=self._sentinel,
|
|
543
694
|
variables=self._variables,
|
|
544
695
|
)
|
|
696
|
+
self._record_dependency_meta(_dependency_key("var", meta, imp.name), meta)
|
|
697
|
+
if library_doc.errors:
|
|
698
|
+
self._record_failed_import(imp, base_dir)
|
|
545
699
|
entry = VariablesEntry(
|
|
546
700
|
name=library_doc.name,
|
|
547
701
|
import_name=imp.name,
|
|
@@ -654,3 +808,15 @@ class ImportResolver:
|
|
|
654
808
|
source=DIAGNOSTICS_SOURCE_NAME,
|
|
655
809
|
code=err.type_name,
|
|
656
810
|
)
|
|
811
|
+
|
|
812
|
+
if entry.library_doc.loaded_without_arguments:
|
|
813
|
+
self._append_diagnostics(
|
|
814
|
+
range=imp.range,
|
|
815
|
+
message=(
|
|
816
|
+
f"Keywords of library '{entry.library_doc.name}' come from loading it without arguments, "
|
|
817
|
+
"because loading it with the import's arguments failed."
|
|
818
|
+
),
|
|
819
|
+
severity=DiagnosticSeverity.INFORMATION,
|
|
820
|
+
source=DIAGNOSTICS_SOURCE_NAME,
|
|
821
|
+
code=Error.LIBRARY_LOADED_WITHOUT_ARGUMENTS,
|
|
822
|
+
)
|