From b6328ca05030d815222b25d208cc59a964623bf9 Mon Sep 17 00:00:00 2001 From: bashonly Date: Sat, 5 Jul 2025 16:55:36 -0500 Subject: [PATCH] [jsinterp] Fix variable scoping (#13639) Authored by: bashonly, seproDev Co-authored-by: sepro --- test/test_jsinterp.py | 46 +++++++++++++++++++++++++++++++++++++++++++ yt_dlp/jsinterp.py | 31 +++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/test/test_jsinterp.py b/test/test_jsinterp.py index 4268e890b8..a1088cea49 100644 --- a/test/test_jsinterp.py +++ b/test/test_jsinterp.py @@ -490,6 +490,52 @@ class TestJSInterpreter(unittest.TestCase): self._test('function f() { var a = "test--"; return a; }', 'test--') self._test('function f() { var b = 1; var a = "b--"; return a; }', 'b--') + def test_nested_function_scoping(self): + self._test(R''' + function f() { + var g = function() { + var P = 2; + return P; + }; + var P = 1; + g(); + return P; + } + ''', 1) + self._test(R''' + function f() { + var x = function() { + for (var w = 1, M = []; w < 2; w++) switch (w) { + case 1: + M.push("a"); + case 2: + M.push("b"); + } + return M + }; + var w = "c"; + var M = "d"; + var y = x(); + y.push(w); + y.push(M); + return y; + } + ''', ['a', 'b', 'c', 'd']) + self._test(R''' + function f() { + var P, Q; + var z = 100; + var g = function() { + var P, Q; P = 2; Q = 15; + z = 0; + return P+Q; + }; + P = 1; Q = 10; + var x = g(), y = 3; + return P+Q+x+y+z; + } + ''', 31) + if __name__ == '__main__': unittest.main() diff --git a/yt_dlp/jsinterp.py b/yt_dlp/jsinterp.py index b49f0cf30a..5b3b33f456 100644 --- a/yt_dlp/jsinterp.py +++ b/yt_dlp/jsinterp.py @@ -222,6 +222,14 @@ class LocalNameSpace(collections.ChainMap): def __delitem__(self, key): raise NotImplementedError('Deleting is not supported') + def set_local(self, key, value): + self.maps[0][key] = value + + def get_local(self, key): + if key in self.maps[0]: + return self.maps[0][key] + return JS_Undefined + class Debugger: import sys @@ -381,7 +389,7 @@ class JSInterpreter: return self._named_object(namespace, obj) @Debugger.wrap_interpreter - def interpret_statement(self, stmt, local_vars, allow_recursion=100): + def interpret_statement(self, stmt, local_vars, allow_recursion=100, _is_var_declaration=False): if allow_recursion < 0: raise self.Exception('Recursion limit reached') allow_recursion -= 1 @@ -401,6 +409,7 @@ class JSInterpreter: if m.group('throw'): raise JS_Throw(self.interpret_expression(expr, local_vars, allow_recursion)) should_return = not m.group('var') + _is_var_declaration = _is_var_declaration or bool(m.group('var')) if not expr: return None, should_return @@ -585,7 +594,8 @@ class JSInterpreter: sub_expressions = list(self._separate(expr)) if len(sub_expressions) > 1: for sub_expr in sub_expressions: - ret, should_abort = self.interpret_statement(sub_expr, local_vars, allow_recursion) + ret, should_abort = self.interpret_statement( + sub_expr, local_vars, allow_recursion, _is_var_declaration=_is_var_declaration) if should_abort: return ret, True return ret, False @@ -599,8 +609,12 @@ class JSInterpreter: left_val = local_vars.get(m.group('out')) if not m.group('index'): - local_vars[m.group('out')] = self._operator( + eval_result = self._operator( m.group('op'), left_val, m.group('expr'), expr, local_vars, allow_recursion) + if _is_var_declaration: + local_vars.set_local(m.group('out'), eval_result) + else: + local_vars[m.group('out')] = eval_result return local_vars[m.group('out')], should_return elif left_val in (None, JS_Undefined): raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) @@ -654,7 +668,16 @@ class JSInterpreter: return float('NaN'), should_return elif m and m.group('return'): - return local_vars.get(m.group('name'), JS_Undefined), should_return + var = m.group('name') + # Declared variables + if _is_var_declaration: + ret = local_vars.get_local(var) + # Register varname in local namespace + # Set value as JS_Undefined or its pre-existing value + local_vars.set_local(var, ret) + else: + ret = local_vars.get(var, JS_Undefined) + return ret, should_return with contextlib.suppress(ValueError): return json.loads(js_to_json(expr, strict=True)), should_return