Skip to content

Commit 228ae4e

Browse files
committed
fix(robotlangserver): speedup analyser
1 parent 4fed028 commit 228ae4e

File tree

4 files changed

+156
-130
lines changed

4 files changed

+156
-130
lines changed

robotcode/language_server/robotframework/diagnostics/analyzer.py

Lines changed: 57 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import asyncio
55
import itertools
66
import os
7-
import re
87
from collections import defaultdict
98
from dataclasses import dataclass
109
from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Union, cast
@@ -20,7 +19,6 @@
2019
Position,
2120
Range,
2221
)
23-
from ...common.text_document import TextDocument
2422
from ..parts.model_helper import ModelHelperMixin
2523
from ..utils.ast_utils import (
2624
HasTokens,
@@ -41,11 +39,14 @@
4139
VariableDefinition,
4240
VariableNotFoundDefinition,
4341
)
44-
from .library_doc import KeywordDoc, is_embedded_keyword
45-
from .namespace import DIAGNOSTICS_SOURCE_NAME, KeywordFinder, Namespace
46-
47-
EXTRACT_COMMENT_PATTERN = re.compile(r".*(?:^ *|\t+| {2,})#(?P<comment>.*)$")
48-
ROBOTCODE_PATTERN = re.compile(r"(?P<marker>\brobotcode\b)\s*:\s*(?P<rule>\b\w+\b)")
42+
from .library_doc import KeywordDoc, KeywordMatcher, is_embedded_keyword
43+
from .namespace import (
44+
DIAGNOSTICS_SOURCE_NAME,
45+
KeywordFinder,
46+
LibraryEntry,
47+
Namespace,
48+
ResourceEntry,
49+
)
4950

5051

5152
@dataclass
@@ -56,20 +57,31 @@ class AnalyzerResult:
5657

5758

5859
class Analyzer(AsyncVisitor, ModelHelperMixin):
59-
def __init__(self, model: ast.AST, namespace: Namespace) -> None:
60+
def __init__(
61+
self,
62+
model: ast.AST,
63+
namespace: Namespace,
64+
finder: KeywordFinder,
65+
ignored_lines: List[int],
66+
libraries_matchers: Dict[KeywordMatcher, LibraryEntry],
67+
resources_matchers: Dict[KeywordMatcher, ResourceEntry],
68+
) -> None:
6069
from robot.parsing.model.statements import Template, TestTemplate
6170

6271
self.model = model
6372
self.namespace = namespace
73+
self.finder = finder
74+
self._ignored_lines = ignored_lines
75+
self.libraries_matchers = libraries_matchers
76+
self.resources_matchers = resources_matchers
77+
6478
self.current_testcase_or_keyword_name: Optional[str] = None
65-
self.finder = KeywordFinder(self.namespace)
6679
self.test_template: Optional[TestTemplate] = None
6780
self.template: Optional[Template] = None
6881
self.node_stack: List[ast.AST] = []
6982
self._diagnostics: List[Diagnostic] = []
7083
self._keyword_references: Dict[KeywordDoc, Set[Location]] = defaultdict(set)
7184
self._variable_references: Dict[VariableDefinition, Set[Location]] = defaultdict(set)
72-
self._ignored_lines: Optional[List[int]] = None
7385

7486
async def run(self) -> AnalyzerResult:
7587
self._diagnostics = []
@@ -167,7 +179,7 @@ async def visit(self, node: ast.AST) -> None:
167179
)
168180

169181
if isinstance(node, Statement) and isinstance(node, KeywordCall) and node.keyword:
170-
kw_doc = await self.finder.find_keyword(node.keyword)
182+
kw_doc = self.finder.find_keyword(node.keyword)
171183
if kw_doc is not None and kw_doc.longname in ["BuiltIn.Comment"]:
172184
severity = DiagnosticSeverity.HINT
173185

@@ -192,7 +204,7 @@ async def visit(self, node: ast.AST) -> None:
192204
return_not_found=True,
193205
):
194206
if isinstance(var, VariableNotFoundDefinition):
195-
await self.append_diagnostics(
207+
self.append_diagnostics(
196208
range=range_from_token(var_token),
197209
message=f"Variable '{var.name}' not found.",
198210
severity=severity,
@@ -203,7 +215,7 @@ async def visit(self, node: ast.AST) -> None:
203215
if isinstance(var, EnvironmentVariableDefinition) and var.default_value is None:
204216
env_name = var.name[2:-1]
205217
if os.environ.get(env_name, None) is None:
206-
await self.append_diagnostics(
218+
self.append_diagnostics(
207219
range=range_from_token(var_token),
208220
message=f"Environment variable '{var.name}' not found.",
209221
severity=severity,
@@ -243,7 +255,7 @@ async def visit(self, node: ast.AST) -> None:
243255
return_not_found=True,
244256
):
245257
if isinstance(var, VariableNotFoundDefinition):
246-
await self.append_diagnostics(
258+
self.append_diagnostics(
247259
range=range_from_token(var_token),
248260
message=f"Variable '{var.name}' not found.",
249261
severity=DiagnosticSeverity.ERROR,
@@ -262,51 +274,16 @@ async def visit(self, node: ast.AST) -> None:
262274
finally:
263275
self.node_stack = self.node_stack[:-1]
264276

265-
@staticmethod
266-
async def get_ignored_lines(document: TextDocument) -> List[int]:
267-
return await document.get_cache(Analyzer.__get_ignored_lines)
268-
269-
@staticmethod
270-
async def __get_ignored_lines(document: TextDocument) -> List[int]:
271-
result = []
272-
lines = await document.get_lines()
273-
for line_no, line in enumerate(lines):
274-
275-
comment = EXTRACT_COMMENT_PATTERN.match(line)
276-
if comment and comment.group("comment"):
277-
for match in ROBOTCODE_PATTERN.finditer(comment.group("comment")):
278-
279-
if match.group("rule") == "ignore":
280-
result.append(line_no)
281-
282-
return result
283-
284-
@classmethod
285-
async def should_ignore(cls, document: Optional[TextDocument], range: Range) -> bool:
286-
return cls.__should_ignore(await cls.get_ignored_lines(document) if document is not None else [], range)
287-
288-
async def _get_ignored_lines(self) -> List[int]:
289-
if self._ignored_lines is None:
290-
self._ignored_lines = (
291-
await Analyzer.get_ignored_lines(self.namespace.document) if self.namespace.document is not None else []
292-
)
293-
294-
return self._ignored_lines
295-
296-
async def _should_ignore(self, range: Range) -> bool:
297-
return self.__should_ignore(await self._get_ignored_lines(), range)
298-
299-
@staticmethod
300-
def __should_ignore(lines: List[int], range: Range) -> bool:
277+
def _should_ignore(self, range: Range) -> bool:
301278
import builtins
302279

303280
for line_no in builtins.range(range.start.line, range.end.line + 1):
304-
if line_no in lines:
281+
if line_no in self._ignored_lines:
305282
return True
306283

307284
return False
308285

309-
async def append_diagnostics(
286+
def append_diagnostics(
310287
self,
311288
range: Range,
312289
message: str,
@@ -319,7 +296,7 @@ async def append_diagnostics(
319296
data: Optional[Any] = None,
320297
) -> None:
321298

322-
if await self._should_ignore(range):
299+
if self._should_ignore(range):
323300
return
324301

325302
self._diagnostics.append(
@@ -356,39 +333,34 @@ async def _analyze_keyword_call(
356333
if not allow_variables and not is_not_variable_token(keyword_token):
357334
return None
358335

359-
if (
360-
await self.namespace.find_keyword(
361-
keyword_token.value, raise_keyword_error=False, handle_bdd_style=False
362-
)
363-
is None
364-
):
336+
if self.finder.find_keyword(keyword_token.value, raise_keyword_error=False, handle_bdd_style=False) is None:
365337
keyword_token = self.strip_bdd_prefix(self.namespace, keyword_token)
366338

367339
kw_range = range_from_token(keyword_token)
368340

369341
if keyword is not None:
370-
libraries_matchers = await self.namespace.get_libraries_matchers()
371-
resources_matchers = await self.namespace.get_resources_matchers()
372342

373343
for lib, name in iter_over_keyword_names_and_owners(keyword):
374344
if (
375345
lib is not None
376-
and not any(k for k in libraries_matchers.keys() if k == lib)
377-
and not any(k for k in resources_matchers.keys() if k == lib)
346+
and not any(k for k in self.libraries_matchers.keys() if k == lib)
347+
and not any(k for k in self.resources_matchers.keys() if k == lib)
378348
):
379349
continue
380350

381-
lib_entry, kw_namespace = await self.get_namespace_info_from_keyword(self.namespace, keyword_token)
351+
lib_entry, kw_namespace = await self.get_namespace_info_from_keyword(
352+
self.namespace, keyword_token, self.libraries_matchers, self.resources_matchers
353+
)
382354
if lib_entry and kw_namespace:
383355
r = range_from_token(keyword_token)
384356
r.end.character = r.start.character + len(kw_namespace)
385357
kw_range.start.character = r.end.character + 1
386358

387-
result = await self.finder.find_keyword(keyword)
359+
result = self.finder.find_keyword(keyword)
388360

389361
if not ignore_errors_if_contains_variables or is_not_variable_token(keyword_token):
390362
for e in self.finder.diagnostics:
391-
await self.append_diagnostics(
363+
self.append_diagnostics(
392364
range=kw_range,
393365
message=e.message,
394366
severity=e.severity,
@@ -400,7 +372,7 @@ async def _analyze_keyword_call(
400372
self._keyword_references[result].add(Location(self.namespace.document.document_uri, kw_range))
401373

402374
if result.errors:
403-
await self.append_diagnostics(
375+
self.append_diagnostics(
404376
range=kw_range,
405377
message="Keyword definition contains errors.",
406378
severity=DiagnosticSeverity.ERROR,
@@ -442,7 +414,7 @@ async def _analyze_keyword_call(
442414
)
443415

444416
if result.is_deprecated:
445-
await self.append_diagnostics(
417+
self.append_diagnostics(
446418
range=kw_range,
447419
message=f"Keyword '{result.name}' is deprecated"
448420
f"{f': {result.deprecated_message}' if result.deprecated_message else ''}.",
@@ -451,14 +423,14 @@ async def _analyze_keyword_call(
451423
code="DeprecatedKeyword",
452424
)
453425
if result.is_error_handler:
454-
await self.append_diagnostics(
426+
self.append_diagnostics(
455427
range=kw_range,
456428
message=f"Keyword definition contains errors: {result.error_handler_message}",
457429
severity=DiagnosticSeverity.ERROR,
458430
code="KeywordContainsErrors",
459431
)
460432
if result.is_reserved():
461-
await self.append_diagnostics(
433+
self.append_diagnostics(
462434
range=kw_range,
463435
message=f"'{result.name}' is a reserved keyword.",
464436
severity=DiagnosticSeverity.ERROR,
@@ -467,7 +439,7 @@ async def _analyze_keyword_call(
467439

468440
if get_robot_version() >= (6, 0, 0) and result.is_resource_keyword and result.is_private():
469441
if self.namespace.source != result.source:
470-
await self.append_diagnostics(
442+
self.append_diagnostics(
471443
range=kw_range,
472444
message=f"Keyword '{result.longname}' is private and should only be called by"
473445
f" keywords in the same file.",
@@ -487,7 +459,7 @@ async def _analyze_keyword_call(
487459
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
488460
raise
489461
except BaseException as e:
490-
await self.append_diagnostics(
462+
self.append_diagnostics(
491463
range=Range(
492464
start=kw_range.start,
493465
end=range_from_token(argument_tokens[-1]).end if argument_tokens else kw_range.end,
@@ -500,7 +472,7 @@ async def _analyze_keyword_call(
500472
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
501473
raise
502474
except BaseException as e:
503-
await self.append_diagnostics(
475+
self.append_diagnostics(
504476
range=range_from_node_or_token(node, keyword_token),
505477
message=str(e),
506478
severity=DiagnosticSeverity.ERROR,
@@ -532,7 +504,7 @@ async def _analyze_keyword_call(
532504
return_not_found=True,
533505
):
534506
if isinstance(var, VariableNotFoundDefinition):
535-
await self.append_diagnostics(
507+
self.append_diagnostics(
536508
range=range_from_token(var_token),
537509
message=f"Variable '{var.name}' not found.",
538510
severity=DiagnosticSeverity.ERROR,
@@ -600,7 +572,7 @@ async def _analyse_run_keyword(
600572
t = argument_tokens[0]
601573
argument_tokens = argument_tokens[1:]
602574
if t.value == "AND":
603-
await self.append_diagnostics(
575+
self.append_diagnostics(
604576
range=range_from_token(t),
605577
message=f"Incorrect use of {t.value}.",
606578
severity=DiagnosticSeverity.ERROR,
@@ -643,7 +615,7 @@ def skip_args() -> List[Token]:
643615

644616
return result
645617

646-
result = await self.finder.find_keyword(argument_tokens[1].value)
618+
result = self.finder.find_keyword(argument_tokens[1].value)
647619

648620
if result is not None and result.is_any_run_keyword():
649621
argument_tokens = argument_tokens[2:]
@@ -769,7 +741,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
769741
keyword_token = cast(RobotToken, value.get_token(RobotToken.KEYWORD))
770742

771743
if value.assign and not value.keyword:
772-
await self.append_diagnostics(
744+
self.append_diagnostics(
773745
range=range_from_node_or_token(value, value.get_token(RobotToken.ASSIGN)),
774746
message="Keyword name cannot be empty.",
775747
severity=DiagnosticSeverity.ERROR,
@@ -781,7 +753,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
781753
)
782754

783755
if not self.current_testcase_or_keyword_name:
784-
await self.append_diagnostics(
756+
self.append_diagnostics(
785757
range=range_from_node_or_token(value, value.get_token(RobotToken.ASSIGN)),
786758
message="Code is unreachable.",
787759
severity=DiagnosticSeverity.HINT,
@@ -800,7 +772,7 @@ async def visit_TestCase(self, node: ast.AST) -> None: # noqa: N802
800772

801773
if not testcase.name:
802774
name_token = cast(TestCaseName, testcase.header).get_token(RobotToken.TESTCASE_NAME)
803-
await self.append_diagnostics(
775+
self.append_diagnostics(
804776
range=range_from_node_or_token(testcase, name_token),
805777
message="Test case name cannot be empty.",
806778
severity=DiagnosticSeverity.ERROR,
@@ -831,15 +803,15 @@ async def visit_Keyword(self, node: ast.AST) -> None: # noqa: N802
831803
if is_embedded_keyword(keyword.name) and any(
832804
isinstance(v, Arguments) and len(v.values) > 0 for v in keyword.body
833805
):
834-
await self.append_diagnostics(
806+
self.append_diagnostics(
835807
range=range_from_node_or_token(keyword, name_token),
836808
message="Keyword cannot have both normal and embedded arguments.",
837809
severity=DiagnosticSeverity.ERROR,
838810
code="KeywordNormalAndEmbbededError",
839811
)
840812
else:
841813
name_token = cast(KeywordName, keyword.header).get_token(RobotToken.KEYWORD_NAME)
842-
await self.append_diagnostics(
814+
self.append_diagnostics(
843815
range=range_from_node_or_token(keyword, name_token),
844816
message="Keyword name cannot be empty.",
845817
severity=DiagnosticSeverity.ERROR,
@@ -878,7 +850,7 @@ async def visit_TemplateArguments(self, node: ast.AST) -> None: # noqa: N802
878850
keyword = template.value
879851
keyword, args = self._format_template(keyword, args)
880852

881-
result = await self.finder.find_keyword(keyword)
853+
result = self.finder.find_keyword(keyword)
882854
if result is not None:
883855
try:
884856
if result.arguments is not None:
@@ -891,15 +863,15 @@ async def visit_TemplateArguments(self, node: ast.AST) -> None: # noqa: N802
891863
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
892864
raise
893865
except BaseException as e:
894-
await self.append_diagnostics(
866+
self.append_diagnostics(
895867
range=range_from_node(arguments, skip_non_data=True),
896868
message=str(e),
897869
severity=DiagnosticSeverity.ERROR,
898870
code=type(e).__qualname__,
899871
)
900872

901873
for d in self.finder.diagnostics:
902-
await self.append_diagnostics(
874+
self.append_diagnostics(
903875
range=range_from_node(arguments, skip_non_data=True),
904876
message=d.message,
905877
severity=d.severity,
@@ -917,7 +889,7 @@ async def visit_Tags(self, node: ast.AST) -> None: # noqa: N802
917889

918890
for tag in tags.get_tokens(RobotToken.ARGUMENT):
919891
if tag.value and tag.value.startswith("-"):
920-
await self.append_diagnostics(
892+
self.append_diagnostics(
921893
range=range_from_node_or_token(node, tag),
922894
message=f"Settings tags starting with a hyphen using the '[Tags]' setting "
923895
f"is deprecated. In Robot Framework 5.2 this syntax will be used "

0 commit comments

Comments
 (0)