diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 3fc91e8b..ac6260ff 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -26,8 +26,10 @@ from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments from ytdl_sub.script.utils.exceptions import InvalidSyntaxException +from ytdl_sub.script.utils.exceptions import InvalidVariableName from ytdl_sub.script.utils.exceptions import UserException from ytdl_sub.script.utils.exceptions import VariableDoesNotExist +from ytdl_sub.script.utils.name_validation import validate_variable_name from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.validators.string_formatter_validators import is_valid_source_variable_name @@ -172,21 +174,17 @@ class _Parser: if _is_breakable(ch): break - is_lower = ch.isascii() and ch.islower() - if not var_name and not is_lower: - raise StringFormattingException("invalid var name") - - if not (is_lower or ch.isnumeric() or ch == "_"): - raise StringFormattingException("invalid var name") + if not var_name: + variable_start_pos = self._pos var_name += ch self._pos += 1 - if not var_name: - raise StringFormattingException("invalid var name") - - if not is_valid_source_variable_name(var_name, raise_exception=False): - raise UNREACHABLE + try: + validate_variable_name(var_name) + except InvalidVariableName: + self._set_highlight_position(variable_start_pos) + raise if self._variable_names is not None and var_name not in self._variable_names: self._set_highlight_position(variable_start_pos) diff --git a/src/ytdl_sub/script/utils/exceptions.py b/src/ytdl_sub/script/utils/exceptions.py index b05976d4..f24a382d 100644 --- a/src/ytdl_sub/script/utils/exceptions.py +++ b/src/ytdl_sub/script/utils/exceptions.py @@ -11,6 +11,14 @@ class InvalidSyntaxException(UserException): """Syntax is incorrect""" +class InvalidVariableName(UserException): + """Variable name is invalid""" + + +class InvalidFunctionName(UserException): + """Custom function name is invalid""" + + class IncompatibleFunctionArguments(UserException): """Function has invalid arguments""" diff --git a/src/ytdl_sub/script/utils/name_validation.py b/src/ytdl_sub/script/utils/name_validation.py new file mode 100644 index 00000000..462bed58 --- /dev/null +++ b/src/ytdl_sub/script/utils/name_validation.py @@ -0,0 +1,50 @@ +import re + +from ytdl_sub.script.functions import Functions +from ytdl_sub.script.utils.exceptions import InvalidFunctionName +from ytdl_sub.script.utils.exceptions import InvalidVariableName + +_NAME_REGEX_VALIDATOR = re.compile(r"^[a-z][a-z0-9_]*$") + + +def is_valid_name(name: str) -> bool: + """ + Returns + ------- + True if the name adheres to the ``snake_case`` format. False otherwise. + """ + return re.match(_NAME_REGEX_VALIDATOR, name) is not None + + +def validate_variable_name(variable_name: str) -> None: + """ + Raises + ------ + InvalidVariableName + if the variable name is invalid + """ + if not is_valid_name(variable_name): + raise InvalidVariableName( + f"Variable name '{variable_name}' is invalid. " + f"Names must be lower_snake_cased and begin with a letter." + ) + + +def validate_custom_function_name(custom_function_name: str) -> None: + """ + Raises + ------ + InvalidVariableName + if the variable name is invalid + """ + if not is_valid_name(custom_function_name): + raise InvalidFunctionName( + f"Custom function name '%{custom_function_name}' is invalid:" + " Names must be %lower_snake_cased and begin with a letter." + ) + + if Functions.is_built_in(custom_function_name): + raise InvalidFunctionName( + f"Custom function name '%{custom_function_name}' is invalid:" + " The name is used by a built-in function and cannot be overwritten." + ) diff --git a/tests/unit/script/types/test_variable.py b/tests/unit/script/types/test_variable.py index 79ad14c0..071ba881 100644 --- a/tests/unit/script/types/test_variable.py +++ b/tests/unit/script/types/test_variable.py @@ -5,6 +5,7 @@ import pytest from ytdl_sub.script.script import Script from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.utils.exceptions import CycleDetected +from ytdl_sub.script.utils.exceptions import InvalidVariableName from ytdl_sub.script.utils.exceptions import VariableDoesNotExist @@ -50,3 +51,13 @@ class TestVariable: match=re.escape("Variable c does not exist."), ): Script({"a": "a", "b": "{c}"}).resolve() + + def test_invalid_variable_name_inline(self): + with pytest.raises( + InvalidVariableName, + match=re.escape( + "Variable name 'vali_LOL_INVALID' is invalid. " + "Names must be lower_snake_cased and begin with a letter." + ), + ): + Script({"a": "{vali_LOL_INVALID}"}).resolve()