Skip to content

Commit ccaefe6

Browse files
committed
implement analysing parameters of keyword calls
1 parent 3a03020 commit ccaefe6

File tree

7 files changed

+126
-32
lines changed

7 files changed

+126
-32
lines changed

robotcode/language_server/robotframework/diagnostics/analyzer.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
from ..utils.ast import (
1919
Token,
2020
is_not_variable_token,
21+
range_from_node,
22+
range_from_node_or_token,
2123
range_from_token,
22-
range_from_token_or_node,
2324
)
2425
from ..utils.async_ast import AsyncVisitor
2526
from .library_doc import KeywordDoc, is_embedded_keyword
@@ -45,7 +46,7 @@ async def visit(self, node: ast.AST) -> None:
4546
async def _analyze_keyword_call(
4647
self,
4748
keyword: Optional[str],
48-
value: ast.AST,
49+
node: ast.AST,
4950
keyword_token: Token,
5051
argument_tokens: List[Token],
5152
analyse_run_keywords: bool = True,
@@ -60,7 +61,7 @@ async def _analyze_keyword_call(
6061
for e in self.finder.diagnostics:
6162
self._results.append(
6263
Diagnostic(
63-
range=range_from_token_or_node(value, keyword_token),
64+
range=range_from_node_or_token(node, keyword_token),
6465
message=e.message,
6566
severity=e.severity,
6667
source=DIAGNOSTICS_SOURCE_NAME,
@@ -72,7 +73,7 @@ async def _analyze_keyword_call(
7273
if result.errors:
7374
self._results.append(
7475
Diagnostic(
75-
range=range_from_token_or_node(value, keyword_token),
76+
range=range_from_node_or_token(node, keyword_token),
7677
message="Keyword definition contains errors.",
7778
severity=DiagnosticSeverity.ERROR,
7879
source=DIAGNOSTICS_SOURCE_NAME,
@@ -117,7 +118,7 @@ async def _analyze_keyword_call(
117118
if result.is_deprecated:
118119
self._results.append(
119120
Diagnostic(
120-
range=range_from_token_or_node(value, keyword_token),
121+
range=range_from_node_or_token(node, keyword_token),
121122
message=f"Keyword '{result.name}' is deprecated"
122123
f"{f': {result.deprecated_message}' if result.deprecated_message else ''}.",
123124
severity=DiagnosticSeverity.HINT,
@@ -128,19 +129,35 @@ async def _analyze_keyword_call(
128129
if result.is_error_handler:
129130
self._results.append(
130131
Diagnostic(
131-
range=range_from_token_or_node(value, keyword_token),
132+
range=range_from_node_or_token(node, keyword_token),
132133
message=f"Keyword definition contains errors: {result.error_handler_message}",
133134
severity=DiagnosticSeverity.ERROR,
134135
source=DIAGNOSTICS_SOURCE_NAME,
135136
)
136137
)
137138

139+
try:
140+
if result.arguments is not None:
141+
result.arguments.resolve([v.value for v in argument_tokens], None)
142+
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
143+
raise
144+
except BaseException as e:
145+
self._results.append(
146+
Diagnostic(
147+
range=range_from_node(node, True),
148+
message=str(e),
149+
severity=DiagnosticSeverity.ERROR,
150+
source=DIAGNOSTICS_SOURCE_NAME,
151+
code=type(e).__qualname__,
152+
)
153+
)
154+
138155
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
139156
raise
140157
except BaseException as e:
141158
self._results.append(
142159
Diagnostic(
143-
range=range_from_token_or_node(value, keyword_token),
160+
range=range_from_node_or_token(node, keyword_token),
144161
message=str(e),
145162
severity=DiagnosticSeverity.ERROR,
146163
source=DIAGNOSTICS_SOURCE_NAME,
@@ -149,7 +166,7 @@ async def _analyze_keyword_call(
149166
)
150167

151168
if result is not None and analyse_run_keywords:
152-
await self._analyse_run_keyword(result, value, argument_tokens)
169+
await self._analyse_run_keyword(result, node, argument_tokens)
153170

154171
return result
155172

@@ -336,7 +353,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
336353
if value.assign and not value.keyword:
337354
self._results.append(
338355
Diagnostic(
339-
range=range_from_token_or_node(value, value.get_token(RobotToken.ASSIGN)),
356+
range=range_from_node_or_token(value, value.get_token(RobotToken.ASSIGN)),
340357
message="Keyword name cannot be empty.",
341358
severity=DiagnosticSeverity.ERROR,
342359
source=DIAGNOSTICS_SOURCE_NAME,
@@ -351,7 +368,7 @@ async def visit_KeywordCall(self, node: ast.AST) -> None: # noqa: N802
351368
if not self.current_testcase_or_keyword_name:
352369
self._results.append(
353370
Diagnostic(
354-
range=range_from_token_or_node(value, value.get_token(RobotToken.ASSIGN)),
371+
range=range_from_node_or_token(value, value.get_token(RobotToken.ASSIGN)),
355372
message="Code is unreachable.",
356373
severity=DiagnosticSeverity.HINT,
357374
source=DIAGNOSTICS_SOURCE_NAME,
@@ -371,7 +388,7 @@ async def visit_TestCase(self, node: ast.AST) -> None: # noqa: N802
371388
name_token = cast(TestCaseName, testcase.header).get_token(RobotToken.TESTCASE_NAME)
372389
self._results.append(
373390
Diagnostic(
374-
range=range_from_token_or_node(testcase, name_token),
391+
range=range_from_node_or_token(testcase, name_token),
375392
message="Test case name cannot be empty.",
376393
severity=DiagnosticSeverity.ERROR,
377394
source=DIAGNOSTICS_SOURCE_NAME,
@@ -398,7 +415,7 @@ async def visit_Keyword(self, node: ast.AST) -> None: # noqa: N802
398415
):
399416
self._results.append(
400417
Diagnostic(
401-
range=range_from_token_or_node(keyword, name_token),
418+
range=range_from_node_or_token(keyword, name_token),
402419
message="Keyword cannot have both normal and embedded arguments.",
403420
severity=DiagnosticSeverity.ERROR,
404421
source=DIAGNOSTICS_SOURCE_NAME,
@@ -409,7 +426,7 @@ async def visit_Keyword(self, node: ast.AST) -> None: # noqa: N802
409426
name_token = cast(KeywordName, keyword.header).get_token(RobotToken.KEYWORD_NAME)
410427
self._results.append(
411428
Diagnostic(
412-
range=range_from_token_or_node(keyword, name_token),
429+
range=range_from_node_or_token(keyword, name_token),
413430
message="Keyword name cannot be empty.",
414431
severity=DiagnosticSeverity.ERROR,
415432
source=DIAGNOSTICS_SOURCE_NAME,

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,69 @@ def __str__(self) -> str:
261261
DEPRECATED_PATTERN = re.compile(r"^\*DEPRECATED(?P<message>.*)\*(?P<doc>.*)")
262262

263263

264+
@dataclass
265+
class ArgumentSpec:
266+
name: str
267+
type: Any
268+
positional_only: Any
269+
positional_or_named: Any
270+
var_positional: Any
271+
named_only: Any
272+
var_named: Any
273+
defaults: Any
274+
types: Any
275+
276+
__robot_arguments: Optional[Any] = None
277+
278+
@staticmethod
279+
def from_robot_argument_spec(spec: Any) -> ArgumentSpec:
280+
return ArgumentSpec(
281+
name=spec.name,
282+
type=spec.type,
283+
positional_only=spec.positional_only,
284+
positional_or_named=spec.positional_or_named,
285+
var_positional=spec.var_positional,
286+
named_only=spec.named_only,
287+
var_named=spec.var_named,
288+
defaults={k: str(v) for k, v in spec.defaults.items()} if spec.defaults else {},
289+
types=None,
290+
)
291+
292+
def resolve(
293+
self,
294+
arguments: Any,
295+
variables: Any,
296+
resolve_named: bool = True,
297+
resolve_variables_until: Any = None,
298+
dict_to_kwargs: bool = False,
299+
) -> Any:
300+
from robot.running.arguments.argumentresolver import ArgumentResolver
301+
from robot.running.arguments.argumentspec import (
302+
ArgumentSpec as RobotArgumentSpec,
303+
)
304+
305+
if self.__robot_arguments is None:
306+
self.__robot_arguments = RobotArgumentSpec(
307+
self.name,
308+
self.type,
309+
self.positional_only,
310+
self.positional_or_named,
311+
self.var_positional,
312+
self.named_only,
313+
self.var_named,
314+
self.defaults,
315+
self.types,
316+
)
317+
318+
resolver = ArgumentResolver(
319+
self.__robot_arguments,
320+
resolve_named=resolve_named,
321+
resolve_variables_until=resolve_variables_until,
322+
dict_to_kwargs=dict_to_kwargs,
323+
)
324+
resolver.resolve(arguments, variables)
325+
326+
264327
@dataclass
265328
class KeywordDoc(Model):
266329
name: str = ""
@@ -282,6 +345,7 @@ class KeywordDoc(Model):
282345
is_registered_run_keyword: bool = False
283346
args_to_process: Optional[int] = None
284347
deprecated: bool = False
348+
arguments: Optional[ArgumentSpec] = None
285349

286350
def __str__(self) -> str:
287351
return f"{self.name}({', '.join(str(arg) for arg in self.args)})"
@@ -1208,6 +1272,7 @@ def get_test_library(
12081272
longname=f"{libdoc.name}.{kw[0].name}",
12091273
doc_format=str(lib.doc_format) or DEFAULT_DOC_FORMAT,
12101274
is_initializer=True,
1275+
arguments=ArgumentSpec.from_robot_argument_spec(kw[1].arguments),
12111276
)
12121277
for kw in [
12131278
(KeywordDocBuilder().build_keyword(k), k) for k in [KeywordWrapper(lib.init, source)]
@@ -1249,6 +1314,7 @@ def get_test_library(
12491314
is_registered_run_keyword=RUN_KW_REGISTER.is_run_keyword(libdoc.name, kw[0].name),
12501315
args_to_process=RUN_KW_REGISTER.get_args_to_process(libdoc.name, kw[0].name),
12511316
deprecated=kw[0].deprecated,
1317+
arguments=ArgumentSpec.from_robot_argument_spec(kw[1].arguments),
12521318
)
12531319
for kw in [
12541320
(KeywordDocBuilder().build_keyword(k), k)

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ async def _import(value: Import) -> Optional[LibraryEntry]:
915915
except (asyncio.CancelledError, SystemExit, KeyboardInterrupt):
916916
raise
917917
except BaseException as e:
918+
self._logger.exception(e)
918919
if top_level:
919920
self._diagnostics.append(
920921
Diagnostic(

robotcode/language_server/robotframework/parts/diagnostics.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ...common.lsp_types import Diagnostic, DiagnosticSeverity, Position, Range
1111
from ...common.parts.diagnostics import DiagnosticsResult
1212
from ...common.text_document import TextDocument
13-
from ..utils.ast import Token, range_from_token
13+
from ..utils.ast import Token, range_from_node, range_from_token
1414

1515
if TYPE_CHECKING:
1616
from ..protocol import RobotLanguageServerProtocol
@@ -77,10 +77,7 @@ async def collect_namespace_diagnostics(
7777

7878
def _create_error_from_node(self, node: ast.AST, msg: str, source: Optional[str] = None) -> Diagnostic:
7979
return Diagnostic(
80-
range=Range(
81-
start=Position(line=node.lineno - 1, character=node.col_offset),
82-
end=Position(line=(node.end_lineno or 1) - 1, character=node.end_col_offset or 0),
83-
),
80+
range=range_from_node(node, True),
8481
message=msg,
8582
severity=DiagnosticSeverity.ERROR,
8683
source=source if source is not None else self.source_name,

robotcode/language_server/robotframework/parts/references.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
get_tokens_at_position,
4646
is_not_variable_token,
4747
iter_over_keyword_names_and_owners,
48+
range_from_node_or_token,
4849
range_from_token,
49-
range_from_token_or_node,
5050
tokenize_variables,
5151
)
5252
from ..utils.async_ast import iter_nodes
@@ -282,7 +282,7 @@ async def references_KeywordName( # noqa: N802
282282
if keyword is not None and keyword.source and not keyword.is_error_handler:
283283
return [
284284
*(
285-
[Location(uri=str(Uri.from_path(keyword.source)), range=range_from_token_or_node(node, name_token))]
285+
[Location(uri=str(Uri.from_path(keyword.source)), range=range_from_node_or_token(node, name_token))]
286286
if context.include_declaration
287287
else []
288288
),

robotcode/language_server/robotframework/utils/ast.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,6 @@ def iter_nodes(node: ast.AST) -> Generator[ast.AST, None, None]:
3232
yield from iter_nodes(value)
3333

3434

35-
def range_from_node(node: ast.AST) -> Range:
36-
return Range(
37-
start=Position(line=node.lineno - 1, character=node.col_offset),
38-
end=Position(
39-
line=node.end_lineno - 1 if node.end_lineno is not None else -1,
40-
character=node.end_col_offset if node.end_col_offset is not None else -1,
41-
),
42-
)
43-
44-
4535
@runtime_checkable
4636
class Token(Protocol):
4737
type: Optional[str]
@@ -114,6 +104,24 @@ def range_from_token(token: Token) -> Range:
114104
)
115105

116106

107+
def range_from_node(node: ast.AST, skip_non_data: bool = False) -> Range:
108+
from robot.parsing.lexer import Token as RobotToken
109+
110+
if skip_non_data and isinstance(node, HasTokens) and node.tokens:
111+
start_token = next((v for v in node.tokens if v.type not in RobotToken.NON_DATA_TOKENS), None)
112+
end_token = next((v for v in reversed(node.tokens) if v.type not in RobotToken.NON_DATA_TOKENS), None)
113+
if start_token is not None and end_token is not None:
114+
return Range(start=range_from_token(start_token).start, end=range_from_token(end_token).end)
115+
116+
return Range(
117+
start=Position(line=node.lineno - 1, character=node.col_offset),
118+
end=Position(
119+
line=node.end_lineno - 1 if node.end_lineno is not None else -1,
120+
character=node.end_col_offset if node.end_col_offset is not None else -1,
121+
),
122+
)
123+
124+
117125
def token_in_range(token: Token, range: Range) -> bool:
118126
token_range = range_from_token(token)
119127
return token_range.start.is_in_range(range) or token_range.end.is_in_range(range)
@@ -124,11 +132,11 @@ def node_in_range(node: ast.AST, range: Range) -> bool:
124132
return node_range.start.is_in_range(range) or node_range.end.is_in_range(range)
125133

126134

127-
def range_from_token_or_node(node: ast.AST, token: Optional[Token]) -> Range:
135+
def range_from_node_or_token(node: ast.AST, token: Optional[Token]) -> Range:
128136
if token is not None:
129137
return range_from_token(token)
130138
if node is not None:
131-
return range_from_node(node)
139+
return range_from_node(node, True)
132140
return Range.zero()
133141

134142

tests/robotcode/language_server/robotframework/parts/data/lib/alibrary.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ def __init__(self, a_param: Any = None) -> None:
1010
def a_library_keyword(self) -> Any:
1111
logger.info("hello from a_library")
1212
return self.a_param
13+
14+
def a_library_keywords_with_args(self, i: int, b: bool) -> Any:
15+
print(i)
16+
print(b)
17+
return (i, b)

0 commit comments

Comments
 (0)