# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ast
import re
import warnings
from abc import ABC, abstractmethod
from collections.abc import Iterator, Sequence
from typing import cast, ClassVar, Literal, overload, TYPE_CHECKING, Type, TypeVar
from robot.conf import Language
from robot.running.arguments import UserKeywordArgumentParser
from robot.utils import normalize_whitespace, seq2str, split_from_equals, test_or_task
from robot.variables import (contains_variable, is_scalar_assign, is_dict_variable,
search_variable)
from ..lexer import Token
if TYPE_CHECKING:
from .blocks import ValidationContext
T = TypeVar('T', bound='Statement')
FOUR_SPACES = ' '
EOL = '\n'
[docs]
class Node(ast.AST, ABC):
_attributes = ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'errors')
lineno: int
col_offset: int
end_lineno: int
end_col_offset: int
errors: 'tuple[str, ...]' = ()
[docs]
class Statement(Node, ABC):
_attributes = ('type', 'tokens') + Node._attributes
type: str
handles_types: 'ClassVar[tuple[str, ...]]' = ()
statement_handlers: 'ClassVar[dict[str, Type[Statement]]]' = {}
# Accepted configuration options. If the value is a tuple, it lists accepted
# values. If the used value contains a variable, it cannot be validated.
options: 'dict[str, tuple|None]' = {}
def __init__(self, tokens: 'Sequence[Token]', errors: 'Sequence[str]' = ()):
self.tokens = tuple(tokens)
self.errors = tuple(errors)
@property
def lineno(self) -> int:
return self.tokens[0].lineno if self.tokens else -1
@property
def col_offset(self) -> int:
return self.tokens[0].col_offset if self.tokens else -1
@property
def end_lineno(self) -> int:
return self.tokens[-1].lineno if self.tokens else -1
@property
def end_col_offset(self) -> int:
return self.tokens[-1].end_col_offset if self.tokens else -1
[docs]
@classmethod
def register(cls, subcls: Type[T]) -> Type[T]:
types = subcls.handles_types or (subcls.type,)
for typ in types:
cls.statement_handlers[typ] = subcls
return subcls
[docs]
@classmethod
def from_tokens(cls, tokens: 'Sequence[Token]') -> 'Statement':
"""Create a statement from given tokens.
Statement type is got automatically from token types.
This classmethod should be called from :class:`Statement`, not from
its subclasses. If you know the subclass to use, simply create an
instance of it directly.
"""
handlers = cls.statement_handlers
for token in tokens:
if token.type in handlers:
return handlers[token.type](tokens)
if any(token.type == Token.ASSIGN for token in tokens):
return KeywordCall(tokens)
return EmptyLine(tokens)
[docs]
@classmethod
@abstractmethod
def from_params(cls, *args, **kwargs) -> 'Statement':
"""Create a statement from passed parameters.
Required and optional arguments in general match class properties.
Values are used to create matching tokens.
Most implementations support following general properties:
- ``separator`` whitespace inserted between each token. Default is four spaces.
- ``indent`` whitespace inserted before first token. Default is four spaces.
- ``eol`` end of line sign. Default is ``'\\n'``.
This classmethod should be called from the :class:`Statement` subclass
to create, not from the :class:`Statement` class itself.
"""
raise NotImplementedError
@property
def data_tokens(self) -> 'list[Token]':
return [t for t in self.tokens if t.type not in Token.NON_DATA_TOKENS]
[docs]
def get_token(self, *types: str) -> 'Token|None':
"""Return a token with any of the given ``types``.
If there are no matches, return ``None``. If there are multiple
matches, return the first match.
"""
for token in self.tokens:
if token.type in types:
return token
return None
[docs]
def get_tokens(self, *types: str) -> 'list[Token]':
"""Return tokens having any of the given ``types``."""
return [t for t in self.tokens if t.type in types]
@overload
def get_value(self, type: str, default: str) -> str:
...
@overload
def get_value(self, type: str, default: None = None) -> 'str|None':
...
[docs]
def get_value(self, type: str, default: 'str|None' = None) -> 'str|None':
"""Return value of a token with the given ``type``.
If there are no matches, return ``default``. If there are multiple
matches, return the value of the first match.
"""
token = self.get_token(type)
return token.value if token else default
[docs]
def get_values(self, *types: str) -> 'tuple[str, ...]':
"""Return values of tokens having any of the given ``types``."""
return tuple(t.value for t in self.tokens if t.type in types)
[docs]
def get_option(self, name: str, default: 'str|None' = None) -> 'str|None':
"""Return value of a configuration option with the given ``name``.
If the option has not been used, return ``default``.
If the option has been used multiple times, values are joined together.
This is typically an error situation and validated elsewhere.
New in Robot Framework 6.1.
"""
return self._get_options().get(name, default)
def _get_options(self) -> 'dict[str, str]':
return dict(opt.split('=', 1) for opt in self.get_values(Token.OPTION))
@property
def lines(self) -> 'Iterator[list[Token]]':
line = []
for token in self.tokens:
line.append(token)
if token.type == Token.EOL:
yield line
line = []
if line:
yield line
[docs]
def validate(self, ctx: 'ValidationContext'):
pass
def _validate_options(self):
for name, value in self._get_options().items():
if self.options[name] is not None:
expected = self.options[name]
if value.upper() not in expected and not contains_variable(value):
self.errors += (f"{self.type} option '{name}' does not accept "
f"value '{value}'. Valid values are "
f"{seq2str(expected)}.",)
def __iter__(self) -> 'Iterator[Token]':
return iter(self.tokens)
def __len__(self) -> int:
return len(self.tokens)
def __getitem__(self, item) -> Token:
return self.tokens[item]
def __repr__(self) -> str:
name = type(self).__name__
tokens = f'tokens={list(self.tokens)}'
errors = f', errors={list(self.errors)}' if self.errors else ''
return f'{name}({tokens}{errors})'
[docs]
class SingleValue(Statement, ABC):
@property
def value(self) -> 'str|None':
values = self.get_values(Token.NAME, Token.ARGUMENT)
if values and values[0].upper() != 'NONE':
return values[0]
return None
[docs]
class MultiValue(Statement, ABC):
@property
def values(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
[docs]
class Fixture(Statement, ABC):
@property
def name(self) -> str:
return self.get_value(Token.NAME, '')
@property
def args(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
[docs]
@Statement.register
class LibraryImport(Statement):
type = Token.LIBRARY
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (), alias: 'str|None' = None,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'LibraryImport':
tokens = [Token(Token.LIBRARY, 'Library'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
if alias is not None:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.AS),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, alias)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, '')
@property
def args(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
@property
def alias(self) -> 'str|None':
separator = self.get_token(Token.AS)
return self.get_tokens(Token.NAME)[-1].value if separator else None
[docs]
@Statement.register
class ResourceImport(Statement):
type = Token.RESOURCE
[docs]
@classmethod
def from_params(cls, name: str, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'ResourceImport':
return cls([
Token(Token.RESOURCE, 'Resource'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name),
Token(Token.EOL, eol)
])
@property
def name(self) -> str:
return self.get_value(Token.NAME, '')
[docs]
@Statement.register
class VariablesImport(Statement):
type = Token.VARIABLES
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
separator: str = FOUR_SPACES, eol: str = EOL) -> 'VariablesImport':
tokens = [Token(Token.VARIABLES, 'Variables'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.NAME, '')
@property
def args(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
[docs]
@Statement.register
class Documentation(DocumentationOrMetadata):
type = Token.DOCUMENTATION
[docs]
@classmethod
def from_params(cls, value: str, indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL,
settings_section: bool = True) -> 'Documentation':
if settings_section:
tokens = [Token(Token.DOCUMENTATION, 'Documentation'),
Token(Token.SEPARATOR, separator)]
else:
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.DOCUMENTATION, '[Documentation]'),
Token(Token.SEPARATOR, separator)]
multiline_separator = ' ' * (len(tokens[-2].value) + len(separator) - 3)
doc_lines = value.splitlines()
if doc_lines:
tokens.extend([Token(Token.ARGUMENT, doc_lines[0]),
Token(Token.EOL, eol)])
for line in doc_lines[1:]:
if not settings_section:
tokens.append(Token(Token.SEPARATOR, indent))
tokens.append(Token(Token.CONTINUATION))
if line:
tokens.append(Token(Token.SEPARATOR, multiline_separator))
tokens.extend([Token(Token.ARGUMENT, line),
Token(Token.EOL, eol)])
return cls(tokens)
[docs]
@Statement.register
class SuiteName(SingleValue):
type = Token.SUITE_NAME
[docs]
@classmethod
def from_params(cls, value: str, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'SuiteName':
return cls([
Token(Token.SUITE_NAME, 'Name'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol)
])
[docs]
@Statement.register
class SuiteSetup(Fixture):
type = Token.SUITE_SETUP
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
separator: str = FOUR_SPACES, eol: str = EOL) -> 'SuiteSetup':
tokens = [Token(Token.SUITE_SETUP, 'Suite Setup'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class SuiteTeardown(Fixture):
type = Token.SUITE_TEARDOWN
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
separator: str = FOUR_SPACES, eol: str = EOL) -> 'SuiteTeardown':
tokens = [Token(Token.SUITE_TEARDOWN, 'Suite Teardown'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class TestSetup(Fixture):
type = Token.TEST_SETUP
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
separator: str = FOUR_SPACES, eol: str = EOL) -> 'TestSetup':
tokens = [Token(Token.TEST_SETUP, 'Test Setup'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class TestTeardown(Fixture):
type = Token.TEST_TEARDOWN
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
separator: str = FOUR_SPACES, eol: str = EOL) -> 'TestTeardown':
tokens = [Token(Token.TEST_TEARDOWN, 'Test Teardown'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class TestTemplate(SingleValue):
type = Token.TEST_TEMPLATE
[docs]
@classmethod
def from_params(cls, value: str, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'TestTemplate':
return cls([
Token(Token.TEST_TEMPLATE, 'Test Template'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol)
])
[docs]
@Statement.register
class TestTimeout(SingleValue):
type = Token.TEST_TIMEOUT
[docs]
@classmethod
def from_params(cls, value: str, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'TestTimeout':
return cls([
Token(Token.TEST_TIMEOUT, 'Test Timeout'),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
Token(Token.EOL, eol)
])
[docs]
@Statement.register
class Variable(Statement):
type = Token.VARIABLE
options = {
'separator': None
}
[docs]
@classmethod
def from_params(cls, name: str,
value: 'str|Sequence[str]',
value_separator: 'str|None' = None,
separator: str = FOUR_SPACES,
eol: str = EOL) -> 'Variable':
values = [value] if isinstance(value, str) else value
tokens = [Token(Token.VARIABLE, name)]
for value in values:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value)])
if value_separator is not None:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f'separator={value_separator}')])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
name = self.get_value(Token.VARIABLE, '')
if name.endswith('='):
return name[:-1].rstrip()
return name
@property
def value(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
@property
def separator(self) -> 'str|None':
return self.get_option('separator')
[docs]
def validate(self, ctx: 'ValidationContext'):
VariableValidator().validate(self)
self._validate_options()
[docs]
@Statement.register
class TestCaseName(Statement):
type = Token.TESTCASE_NAME
[docs]
@classmethod
def from_params(cls, name: str, eol: str = EOL) -> 'TestCaseName':
tokens = [Token(Token.TESTCASE_NAME, name)]
if eol:
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.TESTCASE_NAME, '')
[docs]
def validate(self, ctx: 'ValidationContext'):
if not self.name:
self.errors += (test_or_task('{Test} name cannot be empty.', ctx.tasks),)
[docs]
@Statement.register
class KeywordName(Statement):
type = Token.KEYWORD_NAME
[docs]
@classmethod
def from_params(cls, name: str, eol: str = EOL) -> 'KeywordName':
tokens = [Token(Token.KEYWORD_NAME, name)]
if eol:
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
return self.get_value(Token.KEYWORD_NAME, '')
[docs]
def validate(self, ctx: 'ValidationContext'):
if not self.name:
self.errors += ('User keyword name cannot be empty.',)
[docs]
@Statement.register
class Setup(Fixture):
type = Token.SETUP
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
indent: str = FOUR_SPACES, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'Setup':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.SETUP, '[Setup]'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class Teardown(Fixture):
type = Token.TEARDOWN
[docs]
@classmethod
def from_params(cls, name: str, args: 'Sequence[str]' = (),
indent: str = FOUR_SPACES, separator: str = FOUR_SPACES,
eol: str = EOL) -> 'Teardown':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.TEARDOWN, '[Teardown]'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, name)]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class Template(SingleValue):
type = Token.TEMPLATE
[docs]
@classmethod
def from_params(cls, value: str, indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'Template':
return cls([
Token(Token.SEPARATOR, indent),
Token(Token.TEMPLATE, '[Template]'),
Token(Token.SEPARATOR, separator),
Token(Token.NAME, value),
Token(Token.EOL, eol)
])
[docs]
@Statement.register
class Timeout(SingleValue):
type = Token.TIMEOUT
[docs]
@classmethod
def from_params(cls, value: str, indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'Timeout':
return cls([
Token(Token.SEPARATOR, indent),
Token(Token.TIMEOUT, '[Timeout]'),
Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value),
Token(Token.EOL, eol)
])
[docs]
@Statement.register
class Arguments(MultiValue):
type = Token.ARGUMENTS
[docs]
@classmethod
def from_params(cls, args: 'Sequence[str]', indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'Arguments':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.ARGUMENTS, '[Arguments]')]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
def validate(self, ctx: 'ValidationContext'):
errors: 'list[str]' = []
UserKeywordArgumentParser(error_reporter=errors.append).parse(self.values)
self.errors = tuple(errors)
[docs]
@Statement.register
class ReturnSetting(MultiValue):
"""Represents the deprecated ``[Return]`` setting.
This class was named ``Return`` prior to Robot Framework 7.0. A forward
compatible ``ReturnSetting`` alias existed already in Robot Framework 6.1.
"""
type = Token.RETURN
[docs]
@classmethod
def from_params(cls, args: 'Sequence[str]', indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'ReturnSetting':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.RETURN, '[Return]')]
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
[docs]
@Statement.register
class KeywordCall(Statement):
type = Token.KEYWORD
[docs]
@classmethod
def from_params(cls, name: str, assign: 'Sequence[str]' = (),
args: 'Sequence[str]' = (), indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'KeywordCall':
tokens = [Token(Token.SEPARATOR, indent)]
for assignment in assign:
tokens.extend([Token(Token.ASSIGN, assignment),
Token(Token.SEPARATOR, separator)])
tokens.append(Token(Token.KEYWORD, name))
for arg in args:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def keyword(self) -> str:
return self.get_value(Token.KEYWORD, '')
@property
def args(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
@property
def assign(self) -> 'tuple[str, ...]':
return self.get_values(Token.ASSIGN)
[docs]
@Statement.register
class TemplateArguments(Statement):
type = Token.ARGUMENT
[docs]
@classmethod
def from_params(cls, args: 'Sequence[str]', indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'TemplateArguments':
tokens = []
for index, arg in enumerate(args):
tokens.extend([Token(Token.SEPARATOR, separator if index else indent),
Token(Token.ARGUMENT, arg)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def args(self) -> 'tuple[str, ...]':
return self.get_values(self.type)
[docs]
@Statement.register
class End(NoArgumentHeader):
type = Token.END
[docs]
@Statement.register
class Var(Statement):
type = Token.VAR
options = {
'scope': ('LOCAL', 'TEST', 'TASK', 'SUITE', 'SUITES', 'GLOBAL'),
'separator': None
}
[docs]
@classmethod
def from_params(cls, name: str,
value: 'str|Sequence[str]',
scope: 'str|None' = None,
value_separator: 'str|None' = None,
indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES,
eol: str = EOL) -> 'Var':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.VAR),
Token(Token.SEPARATOR, separator),
Token(Token.VARIABLE, name)]
values = [value] if isinstance(value, str) else value
for value in values:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value)])
if scope:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f'scope={scope}')])
if value_separator:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.OPTION, f'separator={value_separator}')])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def name(self) -> str:
name = self.get_value(Token.VARIABLE, '')
if name.endswith('='):
return name[:-1].rstrip()
return name
@property
def value(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
@property
def scope(self) -> 'str|None':
return self.get_option('scope')
@property
def separator(self) -> 'str|None':
return self.get_option('separator')
[docs]
def validate(self, ctx: 'ValidationContext'):
VariableValidator().validate(self)
self._validate_options()
[docs]
@Statement.register
class Return(Statement):
"""Represents the RETURN statement.
This class named ``ReturnStatement`` prior to Robot Framework 7.0.
The old name still exists as a backwards compatible alias.
"""
type = Token.RETURN_STATEMENT
[docs]
@classmethod
def from_params(cls, values: 'Sequence[str]' = (), indent: str = FOUR_SPACES,
separator: str = FOUR_SPACES, eol: str = EOL) -> 'Return':
tokens = [Token(Token.SEPARATOR, indent),
Token(Token.RETURN_STATEMENT)]
for value in values:
tokens.extend([Token(Token.SEPARATOR, separator),
Token(Token.ARGUMENT, value)])
tokens.append(Token(Token.EOL, eol))
return cls(tokens)
@property
def values(self) -> 'tuple[str, ...]':
return self.get_values(Token.ARGUMENT)
[docs]
def validate(self, ctx: 'ValidationContext'):
if not ctx.in_keyword:
self.errors += ('RETURN can only be used inside a user keyword.',)
if ctx.in_finally:
self.errors += ('RETURN cannot be used in FINALLY branch.',)
# Backwards compatibility with RF < 7.
ReturnStatement = Return
[docs]
class LoopControl(NoArgumentHeader, ABC):
[docs]
def validate(self, ctx: 'ValidationContext'):
super().validate(ctx)
if not ctx.in_loop:
self.errors += (f'{self.type} can only be used inside a loop.',)
if ctx.in_finally:
self.errors += (f'{self.type} cannot be used in FINALLY branch.',)
[docs]
@Statement.register
class Continue(LoopControl):
type = Token.CONTINUE
[docs]
@Statement.register
class Break(LoopControl):
type = Token.BREAK
[docs]
@Statement.register
class Config(Statement):
type = Token.CONFIG
[docs]
@classmethod
def from_params(cls, config: str, eol: str = EOL) -> 'Config':
return cls([
Token(Token.CONFIG, config),
Token(Token.EOL, eol)
])
@property
def language(self) -> 'Language|None':
value = self.get_value(Token.CONFIG)
return Language.from_name(value[len('language:'):]) if value else None
[docs]
@Statement.register
class Error(Statement):
type = Token.ERROR
_errors: 'tuple[str, ...]' = ()
[docs]
@classmethod
def from_params(cls, error: str, value: str = '', indent: str = FOUR_SPACES,
eol: str = EOL) -> 'Error':
return cls([
Token(Token.SEPARATOR, indent),
Token(Token.ERROR, value, error=error),
Token(Token.EOL, eol)
])
@property
def values(self) -> 'list[str]':
return [token.value for token in self.data_tokens]
@property
def errors(self) -> 'tuple[str, ...]':
"""Errors got from the underlying ``ERROR``token.
Errors can be set also explicitly. When accessing errors, they are returned
along with errors got from tokens.
"""
tokens = self.get_tokens(Token.ERROR)
return tuple(t.error or '' for t in tokens) + self._errors
@errors.setter
def errors(self, errors: 'Sequence[str]'):
self._errors = tuple(errors)
[docs]
class EmptyLine(Statement):
type = Token.EOL
[docs]
@classmethod
def from_params(cls, eol: str = EOL):
return cls([Token(Token.EOL, eol)])
[docs]
class VariableValidator:
[docs]
def validate(self, statement: Statement):
name = statement.get_value(Token.VARIABLE, '')
match = search_variable(name, ignore_errors=True)
if not match.is_assign(allow_assign_mark=True, allow_nested=True):
statement.errors += (f"Invalid variable name '{name}'.",)
if match.identifier == '&':
self._validate_dict_items(statement)
def _validate_dict_items(self, statement: Statement):
for item in statement.get_values(Token.ARGUMENT):
if not self._is_valid_dict_item(item):
statement.errors += (
f"Invalid dictionary variable item '{item}'. Items must use "
f"'name=value' syntax or be dictionary variables themselves.",
)
def _is_valid_dict_item(self, item: str) -> bool:
name, value = split_from_equals(item)
return value is not None or is_dict_variable(item)