diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 3284774e..cb6a15f2 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -18,6 +18,7 @@ from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exception_formatters import ParserExceptionFormatter from ytdl_sub.script.utils.exceptions import UNREACHABLE +from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments from ytdl_sub.script.utils.exceptions import InvalidSyntaxException from ytdl_sub.script.utils.exceptions import UserException @@ -334,8 +335,14 @@ class _Parser: while ch := self._read(): if ch == ")": + # Had '(' to indicate there are args if function_args is not None: - # Had '(' to indicate there are args + if self._custom_function_name == function_name: + self._set_highlight_position(function_start_pos) + raise CycleDetected( + f"The custom function %{function_name} cannot call itself." + ) + try: return Function.from_name_and_args(name=function_name, args=function_args) except IncompatibleFunctionArguments: diff --git a/src/ytdl_sub/script/utils/exceptions.py b/src/ytdl_sub/script/utils/exceptions.py index 4c597573..39f9cb79 100644 --- a/src/ytdl_sub/script/utils/exceptions.py +++ b/src/ytdl_sub/script/utils/exceptions.py @@ -19,6 +19,10 @@ class FunctionDoesNotExist(UserException): """Tried to use a function that does not exist""" +class CycleDetected(UserException): + """A cycle exists within a user's script""" + + class FunctionRuntimeException(ValueError): """Exception thrown when a ytdl-sub function has an error occur at runtime""" diff --git a/tests/unit/script/types/test_function.py b/tests/unit/script/types/test_function.py index 4e5bf55e..4ebb206b 100644 --- a/tests/unit/script/types/test_function.py +++ b/tests/unit/script/types/test_function.py @@ -8,6 +8,7 @@ from ytdl_sub.script.types.array import ResolvedArray from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist from ytdl_sub.script.utils.exceptions import FunctionRuntimeException from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments @@ -117,6 +118,14 @@ class TestFunction: ): Script({"dne": "{%throw}"}).resolve() + def test_custom_function_cycle(self): + with pytest.raises( + CycleDetected, match=re.escape("The custom function %cycle_func cannot call itself.") + ): + Script( + {"%cycle_func": "{%mul(%cycle_func(1), $0)}", "output": "{%cycle_func(1)}"} + ).resolve() + def test_lambda_with_custom_function(self): assert Script( {"%times_two": "{%mul($0, 2)}", "wip": "{%array_apply([1, 2, 3], %times_two)}"}