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.
Files changed (32) hide show
  1. robotcode/robot/__version__.py +1 -1
  2. robotcode/robot/config/loader.py +18 -5
  3. robotcode/robot/config/model.py +258 -61
  4. robotcode/robot/diagnostics/analyzer_result.py +1 -0
  5. robotcode/robot/diagnostics/data_cache.py +12 -1
  6. robotcode/robot/diagnostics/diagnostic_rules.py +75 -0
  7. robotcode/robot/diagnostics/diagnostics_modifier.py +83 -43
  8. robotcode/robot/diagnostics/document_cache_helper.py +158 -17
  9. robotcode/robot/diagnostics/errors.py +7 -0
  10. robotcode/robot/diagnostics/import_resolver.py +178 -12
  11. robotcode/robot/diagnostics/imports_manager.py +556 -220
  12. robotcode/robot/diagnostics/library_doc.py +874 -162
  13. robotcode/robot/diagnostics/namespace.py +84 -18
  14. robotcode/robot/diagnostics/namespace_analyzer.py +59 -16
  15. robotcode/robot/diagnostics/project_index.py +88 -13
  16. robotcode/robot/diagnostics/scope_tree.py +2 -2
  17. robotcode/robot/diagnostics/semantic_analyzer/analyzer.py +790 -201
  18. robotcode/robot/diagnostics/semantic_analyzer/enums.py +39 -2
  19. robotcode/robot/diagnostics/semantic_analyzer/json_dump.py +364 -0
  20. robotcode/robot/diagnostics/semantic_analyzer/model.py +72 -11
  21. robotcode/robot/diagnostics/semantic_analyzer/nodes.py +7 -1
  22. robotcode/robot/diagnostics/semantic_analyzer/variable_tokenizer.py +218 -112
  23. robotcode/robot/diagnostics/workspace_config.py +3 -0
  24. robotcode/robot/utils/markdown_docs.py +733 -0
  25. robotcode/robot/utils/markdownformatter.py +57 -12
  26. robotcode/robot/utils/match.py +13 -0
  27. robotcode/robot/utils/robot_patching.py +4 -0
  28. robotcode/robot/utils/variables.py +3 -0
  29. {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/METADATA +4 -3
  30. robotcode_robot-2.8.0.dev548.dist-info/RECORD +48 -0
  31. {robotcode_robot-2.6.2.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/WHEEL +1 -1
  32. 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.get_libdoc_for_library_import(
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 _import_library(self, imp: LibraryImport, base_dir: str, *, top_level: bool) -> None:
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.get_libdoc_for_library_import(
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
- if already_imported is None and (entry.alias or entry.name or entry.import_name) not in self._libraries:
362
- self._libraries[entry.alias or entry.name or entry.import_name] = entry
363
- elif top_level and already_imported and already_imported.library_doc.source:
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.get_resource_doc_for_resource_import(
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.get_libdoc_for_variables_import(
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
+ )