44import asyncio
55import itertools
66import os
7- import re
87from collections import defaultdict
98from dataclasses import dataclass
109from typing import Any , Dict , Generator , List , Optional , Set , Tuple , Union , cast
2019 Position ,
2120 Range ,
2221)
23- from ...common .text_document import TextDocument
2422from ..parts .model_helper import ModelHelperMixin
2523from ..utils .ast_utils import (
2624 HasTokens ,
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
5859class 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