4
4
import asyncio
5
5
import itertools
6
6
import os
7
- import re
8
7
from collections import defaultdict
9
8
from dataclasses import dataclass
10
9
from typing import Any , Dict , Generator , List , Optional , Set , Tuple , Union , cast
20
19
Position ,
21
20
Range ,
22
21
)
23
- from ...common .text_document import TextDocument
24
22
from ..parts .model_helper import ModelHelperMixin
25
23
from ..utils .ast_utils import (
26
24
HasTokens ,
41
39
VariableDefinition ,
42
40
VariableNotFoundDefinition ,
43
41
)
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
+ )
49
50
50
51
51
52
@dataclass
@@ -56,20 +57,31 @@ class AnalyzerResult:
56
57
57
58
58
59
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 :
60
69
from robot .parsing .model .statements import Template , TestTemplate
61
70
62
71
self .model = model
63
72
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
+
64
78
self .current_testcase_or_keyword_name : Optional [str ] = None
65
- self .finder = KeywordFinder (self .namespace )
66
79
self .test_template : Optional [TestTemplate ] = None
67
80
self .template : Optional [Template ] = None
68
81
self .node_stack : List [ast .AST ] = []
69
82
self ._diagnostics : List [Diagnostic ] = []
70
83
self ._keyword_references : Dict [KeywordDoc , Set [Location ]] = defaultdict (set )
71
84
self ._variable_references : Dict [VariableDefinition , Set [Location ]] = defaultdict (set )
72
- self ._ignored_lines : Optional [List [int ]] = None
73
85
74
86
async def run (self ) -> AnalyzerResult :
75
87
self ._diagnostics = []
@@ -167,7 +179,7 @@ async def visit(self, node: ast.AST) -> None:
167
179
)
168
180
169
181
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 )
171
183
if kw_doc is not None and kw_doc .longname in ["BuiltIn.Comment" ]:
172
184
severity = DiagnosticSeverity .HINT
173
185
@@ -192,7 +204,7 @@ async def visit(self, node: ast.AST) -> None:
192
204
return_not_found = True ,
193
205
):
194
206
if isinstance (var , VariableNotFoundDefinition ):
195
- await self .append_diagnostics (
207
+ self .append_diagnostics (
196
208
range = range_from_token (var_token ),
197
209
message = f"Variable '{ var .name } ' not found." ,
198
210
severity = severity ,
@@ -203,7 +215,7 @@ async def visit(self, node: ast.AST) -> None:
203
215
if isinstance (var , EnvironmentVariableDefinition ) and var .default_value is None :
204
216
env_name = var .name [2 :- 1 ]
205
217
if os .environ .get (env_name , None ) is None :
206
- await self .append_diagnostics (
218
+ self .append_diagnostics (
207
219
range = range_from_token (var_token ),
208
220
message = f"Environment variable '{ var .name } ' not found." ,
209
221
severity = severity ,
@@ -243,7 +255,7 @@ async def visit(self, node: ast.AST) -> None:
243
255
return_not_found = True ,
244
256
):
245
257
if isinstance (var , VariableNotFoundDefinition ):
246
- await self .append_diagnostics (
258
+ self .append_diagnostics (
247
259
range = range_from_token (var_token ),
248
260
message = f"Variable '{ var .name } ' not found." ,
249
261
severity = DiagnosticSeverity .ERROR ,
@@ -262,51 +274,16 @@ async def visit(self, node: ast.AST) -> None:
262
274
finally :
263
275
self .node_stack = self .node_stack [:- 1 ]
264
276
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 :
301
278
import builtins
302
279
303
280
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 :
305
282
return True
306
283
307
284
return False
308
285
309
- async def append_diagnostics (
286
+ def append_diagnostics (
310
287
self ,
311
288
range : Range ,
312
289
message : str ,
@@ -319,7 +296,7 @@ async def append_diagnostics(
319
296
data : Optional [Any ] = None ,
320
297
) -> None :
321
298
322
- if await self ._should_ignore (range ):
299
+ if self ._should_ignore (range ):
323
300
return
324
301
325
302
self ._diagnostics .append (
@@ -356,39 +333,34 @@ async def _analyze_keyword_call(
356
333
if not allow_variables and not is_not_variable_token (keyword_token ):
357
334
return None
358
335
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 :
365
337
keyword_token = self .strip_bdd_prefix (self .namespace , keyword_token )
366
338
367
339
kw_range = range_from_token (keyword_token )
368
340
369
341
if keyword is not None :
370
- libraries_matchers = await self .namespace .get_libraries_matchers ()
371
- resources_matchers = await self .namespace .get_resources_matchers ()
372
342
373
343
for lib , name in iter_over_keyword_names_and_owners (keyword ):
374
344
if (
375
345
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 )
378
348
):
379
349
continue
380
350
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
+ )
382
354
if lib_entry and kw_namespace :
383
355
r = range_from_token (keyword_token )
384
356
r .end .character = r .start .character + len (kw_namespace )
385
357
kw_range .start .character = r .end .character + 1
386
358
387
- result = await self .finder .find_keyword (keyword )
359
+ result = self .finder .find_keyword (keyword )
388
360
389
361
if not ignore_errors_if_contains_variables or is_not_variable_token (keyword_token ):
390
362
for e in self .finder .diagnostics :
391
- await self .append_diagnostics (
363
+ self .append_diagnostics (
392
364
range = kw_range ,
393
365
message = e .message ,
394
366
severity = e .severity ,
@@ -400,7 +372,7 @@ async def _analyze_keyword_call(
400
372
self ._keyword_references [result ].add (Location (self .namespace .document .document_uri , kw_range ))
401
373
402
374
if result .errors :
403
- await self .append_diagnostics (
375
+ self .append_diagnostics (
404
376
range = kw_range ,
405
377
message = "Keyword definition contains errors." ,
406
378
severity = DiagnosticSeverity .ERROR ,
@@ -442,7 +414,7 @@ async def _analyze_keyword_call(
442
414
)
443
415
444
416
if result .is_deprecated :
445
- await self .append_diagnostics (
417
+ self .append_diagnostics (
446
418
range = kw_range ,
447
419
message = f"Keyword '{ result .name } ' is deprecated"
448
420
f"{ f': { result .deprecated_message } ' if result .deprecated_message else '' } ." ,
@@ -451,14 +423,14 @@ async def _analyze_keyword_call(
451
423
code = "DeprecatedKeyword" ,
452
424
)
453
425
if result .is_error_handler :
454
- await self .append_diagnostics (
426
+ self .append_diagnostics (
455
427
range = kw_range ,
456
428
message = f"Keyword definition contains errors: { result .error_handler_message } " ,
457
429
severity = DiagnosticSeverity .ERROR ,
458
430
code = "KeywordContainsErrors" ,
459
431
)
460
432
if result .is_reserved ():
461
- await self .append_diagnostics (
433
+ self .append_diagnostics (
462
434
range = kw_range ,
463
435
message = f"'{ result .name } ' is a reserved keyword." ,
464
436
severity = DiagnosticSeverity .ERROR ,
@@ -467,7 +439,7 @@ async def _analyze_keyword_call(
467
439
468
440
if get_robot_version () >= (6 , 0 , 0 ) and result .is_resource_keyword and result .is_private ():
469
441
if self .namespace .source != result .source :
470
- await self .append_diagnostics (
442
+ self .append_diagnostics (
471
443
range = kw_range ,
472
444
message = f"Keyword '{ result .longname } ' is private and should only be called by"
473
445
f" keywords in the same file." ,
@@ -487,7 +459,7 @@ async def _analyze_keyword_call(
487
459
except (asyncio .CancelledError , SystemExit , KeyboardInterrupt ):
488
460
raise
489
461
except BaseException as e :
490
- await self .append_diagnostics (
462
+ self .append_diagnostics (
491
463
range = Range (
492
464
start = kw_range .start ,
493
465
end = range_from_token (argument_tokens [- 1 ]).end if argument_tokens else kw_range .end ,
@@ -500,7 +472,7 @@ async def _analyze_keyword_call(
500
472
except (asyncio .CancelledError , SystemExit , KeyboardInterrupt ):
501
473
raise
502
474
except BaseException as e :
503
- await self .append_diagnostics (
475
+ self .append_diagnostics (
504
476
range = range_from_node_or_token (node , keyword_token ),
505
477
message = str (e ),
506
478
severity = DiagnosticSeverity .ERROR ,
@@ -532,7 +504,7 @@ async def _analyze_keyword_call(
532
504
return_not_found = True ,
533
505
):
534
506
if isinstance (var , VariableNotFoundDefinition ):
535
- await self .append_diagnostics (
507
+ self .append_diagnostics (
536
508
range = range_from_token (var_token ),
537
509
message = f"Variable '{ var .name } ' not found." ,
538
510
severity = DiagnosticSeverity .ERROR ,
@@ -600,7 +572,7 @@ async def _analyse_run_keyword(
600
572
t = argument_tokens [0 ]
601
573
argument_tokens = argument_tokens [1 :]
602
574
if t .value == "AND" :
603
- await self .append_diagnostics (
575
+ self .append_diagnostics (
604
576
range = range_from_token (t ),
605
577
message = f"Incorrect use of { t .value } ." ,
606
578
severity = DiagnosticSeverity .ERROR ,
@@ -643,7 +615,7 @@ def skip_args() -> List[Token]:
643
615
644
616
return result
645
617
646
- result = await self .finder .find_keyword (argument_tokens [1 ].value )
618
+ result = self .finder .find_keyword (argument_tokens [1 ].value )
647
619
648
620
if result is not None and result .is_any_run_keyword ():
649
621
argument_tokens = argument_tokens [2 :]
@@ -769,7 +741,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
769
741
keyword_token = cast (RobotToken , value .get_token (RobotToken .KEYWORD ))
770
742
771
743
if value .assign and not value .keyword :
772
- await self .append_diagnostics (
744
+ self .append_diagnostics (
773
745
range = range_from_node_or_token (value , value .get_token (RobotToken .ASSIGN )),
774
746
message = "Keyword name cannot be empty." ,
775
747
severity = DiagnosticSeverity .ERROR ,
@@ -781,7 +753,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
781
753
)
782
754
783
755
if not self .current_testcase_or_keyword_name :
784
- await self .append_diagnostics (
756
+ self .append_diagnostics (
785
757
range = range_from_node_or_token (value , value .get_token (RobotToken .ASSIGN )),
786
758
message = "Code is unreachable." ,
787
759
severity = DiagnosticSeverity .HINT ,
@@ -800,7 +772,7 @@ async def visit_TestCase(self, node: ast.AST) -> None: # noqa: N802
800
772
801
773
if not testcase .name :
802
774
name_token = cast (TestCaseName , testcase .header ).get_token (RobotToken .TESTCASE_NAME )
803
- await self .append_diagnostics (
775
+ self .append_diagnostics (
804
776
range = range_from_node_or_token (testcase , name_token ),
805
777
message = "Test case name cannot be empty." ,
806
778
severity = DiagnosticSeverity .ERROR ,
@@ -831,15 +803,15 @@ async def visit_Keyword(self, node: ast.AST) -> None: # noqa: N802
831
803
if is_embedded_keyword (keyword .name ) and any (
832
804
isinstance (v , Arguments ) and len (v .values ) > 0 for v in keyword .body
833
805
):
834
- await self .append_diagnostics (
806
+ self .append_diagnostics (
835
807
range = range_from_node_or_token (keyword , name_token ),
836
808
message = "Keyword cannot have both normal and embedded arguments." ,
837
809
severity = DiagnosticSeverity .ERROR ,
838
810
code = "KeywordNormalAndEmbbededError" ,
839
811
)
840
812
else :
841
813
name_token = cast (KeywordName , keyword .header ).get_token (RobotToken .KEYWORD_NAME )
842
- await self .append_diagnostics (
814
+ self .append_diagnostics (
843
815
range = range_from_node_or_token (keyword , name_token ),
844
816
message = "Keyword name cannot be empty." ,
845
817
severity = DiagnosticSeverity .ERROR ,
@@ -878,7 +850,7 @@ async def visit_TemplateArguments(self, node: ast.AST) -> None: # noqa: N802
878
850
keyword = template .value
879
851
keyword , args = self ._format_template (keyword , args )
880
852
881
- result = await self .finder .find_keyword (keyword )
853
+ result = self .finder .find_keyword (keyword )
882
854
if result is not None :
883
855
try :
884
856
if result .arguments is not None :
@@ -891,15 +863,15 @@ async def visit_TemplateArguments(self, node: ast.AST) -> None: # noqa: N802
891
863
except (asyncio .CancelledError , SystemExit , KeyboardInterrupt ):
892
864
raise
893
865
except BaseException as e :
894
- await self .append_diagnostics (
866
+ self .append_diagnostics (
895
867
range = range_from_node (arguments , skip_non_data = True ),
896
868
message = str (e ),
897
869
severity = DiagnosticSeverity .ERROR ,
898
870
code = type (e ).__qualname__ ,
899
871
)
900
872
901
873
for d in self .finder .diagnostics :
902
- await self .append_diagnostics (
874
+ self .append_diagnostics (
903
875
range = range_from_node (arguments , skip_non_data = True ),
904
876
message = d .message ,
905
877
severity = d .severity ,
@@ -917,7 +889,7 @@ async def visit_Tags(self, node: ast.AST) -> None: # noqa: N802
917
889
918
890
for tag in tags .get_tokens (RobotToken .ARGUMENT ):
919
891
if tag .value and tag .value .startswith ("-" ):
920
- await self .append_diagnostics (
892
+ self .append_diagnostics (
921
893
range = range_from_node_or_token (node , tag ),
922
894
message = f"Settings tags starting with a hyphen using the '[Tags]' setting "
923
895
f"is deprecated. In Robot Framework 5.2 this syntax will be used "
0 commit comments