robotcode-robot 2.7.0__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 +229 -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 +76 -8
- robotcode/robot/diagnostics/errors.py +7 -0
- robotcode/robot/diagnostics/import_resolver.py +162 -12
- robotcode/robot/diagnostics/imports_manager.py +318 -97
- robotcode/robot/diagnostics/library_doc.py +874 -162
- robotcode/robot/diagnostics/namespace.py +60 -15
- 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.7.0.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.7.0.dist-info → robotcode_robot-2.8.0.dev548.dist-info}/WHEEL +1 -1
- robotcode_robot-2.7.0.dist-info/RECORD +0 -46
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
"""Shared diagnostic rule helpers."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
from itertools import groupby
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from robot.parsing.lexer.tokens import Token
|
|
8
|
+
from robot.parsing.model.statements import Statement
|
|
9
|
+
|
|
3
10
|
from .entities import VariableDefinition
|
|
11
|
+
from .library_doc import KeywordDoc
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
def is_variable_name_intentionally_unused(variable: VariableDefinition) -> bool:
|
|
@@ -10,3 +18,70 @@ def is_variable_name_intentionally_unused(variable: VariableDefinition) -> bool:
|
|
|
10
18
|
and bool(variable.name_token.value)
|
|
11
19
|
and variable.name_token.value.startswith("_")
|
|
12
20
|
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def private_keyword_call_message(keyword: KeywordDoc, source: Optional[str]) -> Optional[str]:
|
|
24
|
+
"""The `PrivateKeyword` message for a call of `keyword` in the file `source`, or `None` if the call is fine.
|
|
25
|
+
|
|
26
|
+
The file that contains the call decides, not the keyword that calls: Robot Framework also warns when
|
|
27
|
+
a test case calls a private keyword of its own suite file, which is robotframework/robotframework#5807.
|
|
28
|
+
"""
|
|
29
|
+
if not keyword.is_private or keyword.source == source:
|
|
30
|
+
return None
|
|
31
|
+
if keyword.is_resource_keyword:
|
|
32
|
+
return f"Keyword '{keyword.longname}' is private and should only be called by keywords in the same file."
|
|
33
|
+
if keyword.is_library_keyword:
|
|
34
|
+
return f"Keyword '{keyword.longname}' is private and should not be called from Robot Framework files."
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# The section headers of keyword documentation, as Robot Framework 7.5 recognizes them.
|
|
39
|
+
_DOC_SECTION_HEADER = re.compile(
|
|
40
|
+
r"[*_]*(args|arguments|parameters|returns|return|yields|raises|raise|tags)[*_]*::?[*_]*(\s+.*)?",
|
|
41
|
+
re.IGNORECASE,
|
|
42
|
+
)
|
|
43
|
+
_TRAILING_BACKSLASH_OR_NEWLINE = re.compile(r"(\\+)n?$")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def tags_line_without_empty_row(doc: str) -> Optional[int]:
|
|
47
|
+
"""Index of the first `Tags:` line Robot Framework 7.5 warns about, or `None`.
|
|
48
|
+
|
|
49
|
+
Robot Framework accepts a section header only where a section may start:
|
|
50
|
+
on the first line, after an empty line and inside a named section
|
|
51
|
+
(`Args:`, `Returns:`, ...), whose indented and empty lines belong to it.
|
|
52
|
+
A `Tags:` header anywhere else is still used but deprecated.
|
|
53
|
+
"""
|
|
54
|
+
in_named_section = False
|
|
55
|
+
can_start = True
|
|
56
|
+
for index, line in enumerate(doc.splitlines()):
|
|
57
|
+
header = _DOC_SECTION_HEADER.fullmatch(line)
|
|
58
|
+
if header and can_start:
|
|
59
|
+
in_named_section = True
|
|
60
|
+
elif header and header.group(1).lower() == "tags":
|
|
61
|
+
return index
|
|
62
|
+
elif in_named_section and line and not line[:2].isspace():
|
|
63
|
+
in_named_section = False
|
|
64
|
+
can_start = in_named_section or not line.strip()
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def tags_row_without_empty_row(documentation: Statement) -> Optional[List[Token]]:
|
|
69
|
+
"""The tokens of the `[Documentation]` row with a `Tags:` header Robot Framework warns about, or `None`."""
|
|
70
|
+
index = tags_line_without_empty_row(documentation.value)
|
|
71
|
+
if index is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# `value` has one line per row, except that a row ending with a backslash or `\n` continues on the next row
|
|
75
|
+
line = 0
|
|
76
|
+
for _, row_tokens in groupby(documentation.get_tokens(Token.ARGUMENT), key=lambda t: t.lineno):
|
|
77
|
+
row = list(row_tokens)
|
|
78
|
+
if line == index:
|
|
79
|
+
return row
|
|
80
|
+
if not _has_trailing_backslash_or_newline(row[-1].value):
|
|
81
|
+
line += 1
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _has_trailing_backslash_or_newline(value: str) -> bool:
|
|
86
|
+
match = _TRAILING_BACKSLASH_OR_NEWLINE.search(value)
|
|
87
|
+
return bool(match and len(match.group(1)) % 2 == 1)
|
|
@@ -4,7 +4,7 @@ from ast import AST
|
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Dict, List, Optional, Set, Union
|
|
7
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
8
8
|
|
|
9
9
|
from robot.parsing.lexer.tokens import Token
|
|
10
10
|
from robot.parsing.model.blocks import Block, File
|
|
@@ -13,7 +13,8 @@ from robotcode.core.lsp.types import Diagnostic, DiagnosticSeverity
|
|
|
13
13
|
|
|
14
14
|
from ..utils.visitor import Visitor
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# The long names come before their short forms, the pattern takes the first alternative that matches.
|
|
17
|
+
ACTIONS = ["ignore", "error", "warning", "warn", "information", "info", "hint", "reset"]
|
|
17
18
|
|
|
18
19
|
ROBOTCODE_ACTION_AND_CODES_PATTERN = re.compile(rf"(?P<action>{'|'.join(ACTIONS)})(\[(?P<codes>[^\]]*?)\])?")
|
|
19
20
|
|
|
@@ -35,10 +36,22 @@ class ModifierAction(Enum):
|
|
|
35
36
|
return cls(value)
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
# How near a modifier is to the line it affects: an inline modifier at the end of the line, an indented comment in
|
|
40
|
+
# a block, or a top-level comment of the file.
|
|
41
|
+
_FILE_LEVEL = 0
|
|
42
|
+
_BLOCK_LEVEL = 1
|
|
43
|
+
_INLINE_LEVEL = 2
|
|
44
|
+
|
|
45
|
+
# The rank of a modifier: its level, then the line of its comment, then its position in the comment. The higher rank
|
|
46
|
+
# wins, also between a modifier for a code and one without a list (`*`).
|
|
47
|
+
Rank = Tuple[int, int, int]
|
|
48
|
+
|
|
49
|
+
|
|
38
50
|
@dataclass
|
|
39
51
|
class RulesAndCodes:
|
|
40
52
|
codes: Dict[Union[str, int], Set[int]]
|
|
41
53
|
actions: Dict[int, Dict[Union[str, int], ModifierAction]]
|
|
54
|
+
ranks: Dict[int, Dict[Union[str, int], Rank]] = field(default_factory=lambda: defaultdict(dict))
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
_translation_table = str.maketrans("", "", "_- ")
|
|
@@ -79,15 +92,19 @@ class ModifiersVisitor(Visitor):
|
|
|
79
92
|
self.generic_visit(node)
|
|
80
93
|
|
|
81
94
|
def visit_Block(self, node: Block) -> None: # noqa: N802
|
|
95
|
+
# Restored afterwards, so that a modifier after a nested block (IF, FOR, ...) belongs to the outer block.
|
|
96
|
+
parent_block = self.current_block
|
|
82
97
|
self.current_block = node
|
|
83
98
|
self.generic_visit(node)
|
|
99
|
+
self.current_block = parent_block
|
|
84
100
|
|
|
85
101
|
def visit_Statement(self, node: Statement) -> None: # noqa: N802
|
|
86
102
|
self._handle_statement_comments(node)
|
|
87
103
|
self.generic_visit(node)
|
|
88
104
|
|
|
89
|
-
def
|
|
90
|
-
|
|
105
|
+
def _parse_actions(self, comment: str) -> List[Tuple[ModifierAction, str]]:
|
|
106
|
+
"""The actions of a modifier comment and their codes, in the order they are written."""
|
|
107
|
+
result: List[Tuple[ModifierAction, str]] = []
|
|
91
108
|
|
|
92
109
|
comment = comment.strip()
|
|
93
110
|
m = ROBOTCODE_ACTION_AND_CODES_PATTERN.match(comment)
|
|
@@ -98,17 +115,35 @@ class ModifiersVisitor(Visitor):
|
|
|
98
115
|
action_str = m.group("action")
|
|
99
116
|
action = ModifierAction.from_str(action_str)
|
|
100
117
|
messages = m.group("codes")
|
|
101
|
-
|
|
118
|
+
codes = (
|
|
102
119
|
[m.strip().translate(_translation_table).lower() for m in messages.split(",")]
|
|
103
120
|
if messages is not None
|
|
104
121
|
else ["*"]
|
|
105
122
|
)
|
|
123
|
+
for code in codes:
|
|
124
|
+
# A code belongs to the rightmost action that names it, also when an action appears more than once.
|
|
125
|
+
result = [entry for entry in result if entry[1] != code]
|
|
126
|
+
result.append((action, code))
|
|
106
127
|
|
|
107
128
|
return result
|
|
108
129
|
|
|
130
|
+
def _parse_robotcode_disabler(self, comment: str) -> Dict[ModifierAction, List[str]]:
|
|
131
|
+
result: Dict[ModifierAction, List[str]] = {}
|
|
132
|
+
for action, code in self._parse_actions(comment):
|
|
133
|
+
result.setdefault(action, []).append(code)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def _add_rule(self, lineno: int, code: str, action: ModifierAction, rank: Rank) -> None:
|
|
137
|
+
self.rules_and_codes.codes[code].add(lineno)
|
|
138
|
+
current = self.rules_and_codes.ranks[lineno].get(code)
|
|
139
|
+
if current is None or rank >= current:
|
|
140
|
+
self.rules_and_codes.actions[lineno][code] = action
|
|
141
|
+
self.rules_and_codes.ranks[lineno][code] = rank
|
|
142
|
+
|
|
109
143
|
def _handle_statement_comments(self, node: Statement) -> None:
|
|
110
144
|
first_comment = True
|
|
111
145
|
has_marker = False
|
|
146
|
+
position = 0
|
|
112
147
|
|
|
113
148
|
for token in node.get_tokens(Token.COMMENT):
|
|
114
149
|
value = token.value.strip()
|
|
@@ -127,15 +162,15 @@ class ModifiersVisitor(Visitor):
|
|
|
127
162
|
continue
|
|
128
163
|
|
|
129
164
|
if has_marker:
|
|
130
|
-
actions = self.
|
|
165
|
+
actions = self._parse_actions(value)
|
|
131
166
|
|
|
132
167
|
if not actions:
|
|
133
168
|
break
|
|
134
169
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
lineno = token.lineno - 1
|
|
171
|
+
for action, code in actions:
|
|
172
|
+
self._add_rule(lineno, code, action, (_INLINE_LEVEL, lineno, position))
|
|
173
|
+
position += 1
|
|
139
174
|
|
|
140
175
|
def _handle_comment(self, node: Comment) -> None:
|
|
141
176
|
first_comment = True
|
|
@@ -143,9 +178,12 @@ class ModifiersVisitor(Visitor):
|
|
|
143
178
|
|
|
144
179
|
start_lineno = node.lineno - 1
|
|
145
180
|
end_lineno = self.file_end_lineno
|
|
181
|
+
level = _FILE_LEVEL
|
|
182
|
+
position = 0
|
|
146
183
|
|
|
147
184
|
if node.tokens[0].type == Token.SEPARATOR and self.current_block is not None:
|
|
148
185
|
end_lineno = self.current_block.end_lineno - 1
|
|
186
|
+
level = _BLOCK_LEVEL
|
|
149
187
|
for token in node.get_tokens(Token.COMMENT):
|
|
150
188
|
value = token.value.strip()
|
|
151
189
|
if first_comment and value.startswith("#"):
|
|
@@ -163,16 +201,16 @@ class ModifiersVisitor(Visitor):
|
|
|
163
201
|
continue
|
|
164
202
|
|
|
165
203
|
if has_marker:
|
|
166
|
-
actions = self.
|
|
204
|
+
actions = self._parse_actions(value)
|
|
167
205
|
|
|
168
206
|
if not actions:
|
|
169
207
|
break
|
|
170
208
|
|
|
171
|
-
for action,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
for action, code in actions:
|
|
210
|
+
rank = (level, start_lineno, position)
|
|
211
|
+
position += 1
|
|
212
|
+
for i in range(start_lineno, end_lineno + 1):
|
|
213
|
+
self._add_rule(i, code, action, rank)
|
|
176
214
|
|
|
177
215
|
|
|
178
216
|
@dataclass
|
|
@@ -211,34 +249,25 @@ class DiagnosticsModifier:
|
|
|
211
249
|
else "unknowncode"
|
|
212
250
|
)
|
|
213
251
|
|
|
214
|
-
lines = self.rules_and_codes.codes.get(code)
|
|
215
|
-
|
|
216
|
-
if lines is None or (lines is not None and diagnostic.range.start.line not in lines):
|
|
217
|
-
code = "*"
|
|
218
|
-
lines = self.rules_and_codes.codes.get(code)
|
|
219
|
-
|
|
220
252
|
modified = False
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
elif action == ModifierAction.HINT:
|
|
240
|
-
modified = True
|
|
241
|
-
diagnostic.severity = DiagnosticSeverity.HINT
|
|
253
|
+
action = self._action_for(diagnostic.range.start.line, code)
|
|
254
|
+
if action is not None:
|
|
255
|
+
if action == ModifierAction.IGNORE:
|
|
256
|
+
return None
|
|
257
|
+
if action == ModifierAction.RESET:
|
|
258
|
+
pass
|
|
259
|
+
elif action == ModifierAction.ERROR:
|
|
260
|
+
modified = True
|
|
261
|
+
diagnostic.severity = DiagnosticSeverity.ERROR
|
|
262
|
+
elif action == ModifierAction.WARNING:
|
|
263
|
+
modified = True
|
|
264
|
+
diagnostic.severity = DiagnosticSeverity.WARNING
|
|
265
|
+
elif action == ModifierAction.INFORMATION:
|
|
266
|
+
modified = True
|
|
267
|
+
diagnostic.severity = DiagnosticSeverity.INFORMATION
|
|
268
|
+
elif action == ModifierAction.HINT:
|
|
269
|
+
modified = True
|
|
270
|
+
diagnostic.severity = DiagnosticSeverity.HINT
|
|
242
271
|
|
|
243
272
|
if not modified:
|
|
244
273
|
if orig_code in self.config.ignore:
|
|
@@ -264,5 +293,16 @@ class DiagnosticsModifier:
|
|
|
264
293
|
|
|
265
294
|
return diagnostic
|
|
266
295
|
|
|
296
|
+
def _action_for(self, line: int, code: str) -> Optional[ModifierAction]:
|
|
297
|
+
"""The action of the nearest modifier on `line` for `code`: one that names the code, or one without a list."""
|
|
298
|
+
actions = self.rules_and_codes.actions.get(line)
|
|
299
|
+
if not actions:
|
|
300
|
+
return None
|
|
301
|
+
ranks = self.rules_and_codes.ranks.get(line, {})
|
|
302
|
+
keys = [key for key in (code, "*") if key in actions]
|
|
303
|
+
if not keys:
|
|
304
|
+
return None
|
|
305
|
+
return actions[max(keys, key=lambda key: ranks.get(key, (-1, -1, -1)))]
|
|
306
|
+
|
|
267
307
|
def modify_diagnostics(self, diagnostics: List[Diagnostic]) -> List[Diagnostic]:
|
|
268
308
|
return [d for d in map(self.modify_diagnostic, diagnostics) if d is not None]
|
|
@@ -8,6 +8,7 @@ from logging import CRITICAL
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
+
Dict,
|
|
11
12
|
Iterable,
|
|
12
13
|
Iterator,
|
|
13
14
|
List,
|
|
@@ -24,7 +25,7 @@ from robotcode.core.filewatcher import FileWatcherManagerBase
|
|
|
24
25
|
from robotcode.core.text_document import TextDocument
|
|
25
26
|
from robotcode.core.uri import Uri
|
|
26
27
|
from robotcode.core.utils.logging import LoggingDescriptor
|
|
27
|
-
from robotcode.core.utils.path import DiskInfo
|
|
28
|
+
from robotcode.core.utils.path import DiskInfo, normalized_path
|
|
28
29
|
from robotcode.core.workspace import Workspace, WorkspaceFolder
|
|
29
30
|
from robotcode.robot.diagnostics.diagnostics_modifier import (
|
|
30
31
|
DiagnosticModifiersConfig,
|
|
@@ -66,11 +67,16 @@ def _namespace_cache_key(source: str, document_type: Optional[DocumentType]) ->
|
|
|
66
67
|
a suite (GENERAL) and as an imported resource (RESOURCE) — the results
|
|
67
68
|
must not overwrite each other. "\\n" as separator is impossible in Windows
|
|
68
69
|
paths and never appears in real-world POSIX paths (where it is technically
|
|
69
|
-
legal); the key is
|
|
70
|
+
legal); the key is parsed back only to tell the source of an entry.
|
|
70
71
|
"""
|
|
71
72
|
return f"{source}\n{document_type.value if document_type is not None else ''}"
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
def _namespace_cache_source(entry_name: str) -> str:
|
|
76
|
+
"""The source of a namespace entry in the disk cache, see `_namespace_cache_key`."""
|
|
77
|
+
return entry_name.split("\n", 1)[0]
|
|
78
|
+
|
|
79
|
+
|
|
74
80
|
class _CacheEntry:
|
|
75
81
|
pass
|
|
76
82
|
|
|
@@ -121,15 +127,30 @@ class DocumentsCacheHelper:
|
|
|
121
127
|
|
|
122
128
|
with self._project_indexes_lock:
|
|
123
129
|
if folder not in self._project_indexes:
|
|
124
|
-
self._project_indexes[folder] =
|
|
130
|
+
self._project_indexes[folder] = self._create_project_index()
|
|
125
131
|
return self._project_indexes[folder]
|
|
126
132
|
|
|
127
133
|
def default_project_index(self) -> ProjectIndex:
|
|
128
134
|
with self._project_indexes_lock:
|
|
129
135
|
if self._default_project_index is None:
|
|
130
|
-
self._default_project_index =
|
|
136
|
+
self._default_project_index = self._create_project_index()
|
|
131
137
|
return self._default_project_index
|
|
132
138
|
|
|
139
|
+
def _create_project_index(self) -> ProjectIndex:
|
|
140
|
+
documents_manager = self.documents_manager
|
|
141
|
+
# converting a path is most of the cost of a lookup, and the URI of a file does not change
|
|
142
|
+
uris: Dict[str, Uri] = {}
|
|
143
|
+
|
|
144
|
+
# only the files the documents manager still holds count; the check refers to the documents manager and
|
|
145
|
+
# not to this helper, which would make the helper and its indexes a reference cycle
|
|
146
|
+
def is_known_source(source: str) -> bool:
|
|
147
|
+
uri = uris.get(source)
|
|
148
|
+
if uri is None:
|
|
149
|
+
uri = uris[source] = Uri.from_path(source).normalized()
|
|
150
|
+
return documents_manager.get(uri) is not None
|
|
151
|
+
|
|
152
|
+
return ProjectIndex(is_known_source)
|
|
153
|
+
|
|
133
154
|
def get_languages_for_document(self, document_or_uri: Union[TextDocument, Uri, str]) -> Optional[Languages]:
|
|
134
155
|
if RF_VERSION < (6, 0):
|
|
135
156
|
return None
|
|
@@ -418,9 +439,7 @@ class DocumentsCacheHelper:
|
|
|
418
439
|
def namespace_invalidated(sender, namespace: Namespace) -> None: ...
|
|
419
440
|
|
|
420
441
|
def _invalidate_namespace(self, sender: Namespace) -> None:
|
|
421
|
-
|
|
422
|
-
self.get_project_index(sender.document).remove_file(sender.source)
|
|
423
|
-
|
|
442
|
+
# the file's references stay in the index until its next analysis replaces them
|
|
424
443
|
document = sender.document
|
|
425
444
|
if document is not None:
|
|
426
445
|
document.remove_cache_entry(self.__get_general_namespace)
|
|
@@ -634,6 +653,53 @@ class DocumentsCacheHelper:
|
|
|
634
653
|
|
|
635
654
|
return result
|
|
636
655
|
|
|
656
|
+
def remove_cached_analysis(self, path: Path) -> None:
|
|
657
|
+
"""Remove the entries of a Robot Framework file from the analysis cache, as for a file that was deleted."""
|
|
658
|
+
uri = Uri.from_path(path).normalized()
|
|
659
|
+
source = str(uri.to_path())
|
|
660
|
+
try:
|
|
661
|
+
imports_manager = self.get_imports_manager_for_uri(uri)
|
|
662
|
+
imports_manager.data_cache.remove_entries(
|
|
663
|
+
CacheSection.NAMESPACE, [_namespace_cache_key(source, document_type) for document_type in DocumentType]
|
|
664
|
+
)
|
|
665
|
+
imports_manager.remove_cached_resource_docs([str(normalized_path(source))])
|
|
666
|
+
except (SystemExit, KeyboardInterrupt):
|
|
667
|
+
raise
|
|
668
|
+
except BaseException as e:
|
|
669
|
+
ex = e
|
|
670
|
+
self._logger.debug(
|
|
671
|
+
lambda: f"Failed to remove the analysis cache entries of {source}: {ex}",
|
|
672
|
+
context_name="cache",
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def remove_cached_analyses_of_missing_files(self, folder: WorkspaceFolder) -> None:
|
|
676
|
+
"""Remove the entries of the files that do not exist from the analysis cache of a workspace folder."""
|
|
677
|
+
imports_manager = self.get_imports_manager_for_workspace_folder(folder)
|
|
678
|
+
data_cache = imports_manager.data_cache
|
|
679
|
+
try:
|
|
680
|
+
for section, source_of in (
|
|
681
|
+
(CacheSection.NAMESPACE, _namespace_cache_source),
|
|
682
|
+
(CacheSection.RESOURCE, str),
|
|
683
|
+
):
|
|
684
|
+
missing = [
|
|
685
|
+
entry.entry_name
|
|
686
|
+
for entry in data_cache.list_entries(section)
|
|
687
|
+
if not Path(source_of(entry.entry_name)).exists()
|
|
688
|
+
]
|
|
689
|
+
if not missing:
|
|
690
|
+
continue
|
|
691
|
+
if section == CacheSection.RESOURCE:
|
|
692
|
+
imports_manager.remove_cached_resource_docs(missing)
|
|
693
|
+
else:
|
|
694
|
+
data_cache.remove_entries(section, missing)
|
|
695
|
+
except (SystemExit, KeyboardInterrupt):
|
|
696
|
+
raise
|
|
697
|
+
except BaseException as e:
|
|
698
|
+
ex = e
|
|
699
|
+
self._logger.debug(
|
|
700
|
+
lambda: f"Failed to remove the cache entries of missing files: {ex}", context_name="cache"
|
|
701
|
+
)
|
|
702
|
+
|
|
637
703
|
def _save_namespace_to_cache(
|
|
638
704
|
self,
|
|
639
705
|
source: str,
|
|
@@ -780,7 +846,9 @@ class DocumentsCacheHelper:
|
|
|
780
846
|
def calc_cache_path(self, folder_uri: Uri) -> Path:
|
|
781
847
|
from .data_cache import resolve_cache_base_path
|
|
782
848
|
|
|
783
|
-
|
|
849
|
+
folder = folder_uri.to_path()
|
|
850
|
+
cache_dir = self.analysis_config.cache_dir
|
|
851
|
+
return resolve_cache_base_path(folder / cache_dir if cache_dir else folder)
|
|
784
852
|
|
|
785
853
|
def get_diagnostic_modifier(self, document: TextDocument) -> DiagnosticsModifier:
|
|
786
854
|
return document.get_cache(self.__get_diagnostic_modifier)
|
|
@@ -20,6 +20,12 @@ class Error:
|
|
|
20
20
|
VARIABLES_ALREADY_IMPORTED = "VariablesAlreadyImported"
|
|
21
21
|
LIBRARY_ALREADY_IMPORTED = "LibraryAlreadyImported"
|
|
22
22
|
LIBRARY_OVERRIDES_BUILTIN = "LibraryOverridesBuiltIn"
|
|
23
|
+
LIBRARY_IMPORT_IGNORED = "LibraryImportIgnored"
|
|
24
|
+
LIBRARY_LOADED_WITHOUT_ARGUMENTS = "LibraryLoadedWithoutArguments"
|
|
25
|
+
LIBRARY_TIMEOUT_ERROR = "LibraryTimeoutError"
|
|
26
|
+
VARIABLES_TIMEOUT_ERROR = "VariablesTimeoutError"
|
|
27
|
+
LIBRARY_EXIT_ERROR = "LibraryExitError"
|
|
28
|
+
VARIABLES_EXIT_ERROR = "VariablesExitError"
|
|
23
29
|
DEPRECATED_KEYWORD = "DeprecatedKeyword"
|
|
24
30
|
KEYWORD_CONTAINS_ERRORS = "KeywordContainsErrors"
|
|
25
31
|
RESERVED_KEYWORD = "ReservedKeyword"
|
|
@@ -32,6 +38,7 @@ class Error:
|
|
|
32
38
|
DEPRECATED_HYPHEN_TAG = "DeprecatedHyphenTag"
|
|
33
39
|
DEPRECATED_RETURN_SETTING = "DeprecatedReturnSetting"
|
|
34
40
|
DEPRECATED_FORCE_TAG = "DeprecatedForceTag"
|
|
41
|
+
TAGS_WITHOUT_EMPTY_ROW = "TagsWithoutEmptyRow"
|
|
35
42
|
IMPORT_REQUIRES_VALUE = "ImportRequiresValue"
|
|
36
43
|
KEYWORD_ERROR = "KeywordError"
|
|
37
44
|
MULTIPLE_KEYWORDS = "MultipleKeywords"
|