mirror of
https://github.com/ytdl-org/youtube-dl
synced 2024-12-28 10:15:49 +01:00
[jsinterp] Fix and improve loose and strict equality operations
* reimplement loose equality according to MDN (eg, 1 == "1") * improve strict equality (eg, "abc" === "abc" but 'abc' is not 'abc') * add tests for above
This commit is contained in:
parent
118c6d7a17
commit
c1a03b1ac3
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
@ -11,7 +12,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import math
|
||||
import re
|
||||
|
||||
from youtube_dl.compat import compat_str
|
||||
from youtube_dl.compat import compat_str as str
|
||||
from youtube_dl.jsinterp import JS_Undefined, JSInterpreter
|
||||
|
||||
NaN = object()
|
||||
@ -19,7 +20,7 @@ NaN = object()
|
||||
|
||||
class TestJSInterpreter(unittest.TestCase):
|
||||
def _test(self, jsi_or_code, expected, func='f', args=()):
|
||||
if isinstance(jsi_or_code, compat_str):
|
||||
if isinstance(jsi_or_code, str):
|
||||
jsi_or_code = JSInterpreter(jsi_or_code)
|
||||
got = jsi_or_code.call_function(func, *args)
|
||||
if expected is NaN:
|
||||
@ -89,7 +90,35 @@ class TestJSInterpreter(unittest.TestCase):
|
||||
self._test('function f(){return 19 & 21;}', 17)
|
||||
self._test('function f(){return 11 >> 2;}', 2)
|
||||
self._test('function f(){return []? 2+3: 4;}', 5)
|
||||
# equality
|
||||
self._test('function f(){return 1 == 1}', True)
|
||||
self._test('function f(){return 1 == 1.0}', True)
|
||||
self._test('function f(){return 1 == "1"}', True)
|
||||
self._test('function f(){return 1 == 2}', False)
|
||||
self._test('function f(){return 1 != "1"}', False)
|
||||
self._test('function f(){return 1 != 2}', True)
|
||||
self._test('function f(){var x = {a: 1}; var y = x; return x == y}', True)
|
||||
self._test('function f(){var x = {a: 1}; return x == {a: 1}}', False)
|
||||
self._test('function f(){return NaN == NaN}', False)
|
||||
self._test('function f(){return null == undefined}', True)
|
||||
self._test('function f(){return "spam, eggs" == "spam, eggs"}', True)
|
||||
# strict equality
|
||||
self._test('function f(){return 1 === 1}', True)
|
||||
self._test('function f(){return 1 === 1.0}', True)
|
||||
self._test('function f(){return 1 === "1"}', False)
|
||||
self._test('function f(){return 1 === 2}', False)
|
||||
self._test('function f(){var x = {a: 1}; var y = x; return x === y}', True)
|
||||
self._test('function f(){var x = {a: 1}; return x === {a: 1}}', False)
|
||||
self._test('function f(){return NaN === NaN}', False)
|
||||
self._test('function f(){return null === undefined}', False)
|
||||
self._test('function f(){return null === null}', True)
|
||||
self._test('function f(){return undefined === undefined}', True)
|
||||
self._test('function f(){return "uninterned" === "uninterned"}', True)
|
||||
self._test('function f(){return 1 === 1}', True)
|
||||
self._test('function f(){return 1 === "1"}', False)
|
||||
self._test('function f(){return 1 !== 1}', False)
|
||||
self._test('function f(){return 1 !== "1"}', True)
|
||||
# expressions
|
||||
self._test('function f(){return 0 && 1 || 2;}', 2)
|
||||
self._test('function f(){return 0 ?? 42;}', 0)
|
||||
self._test('function f(){return "life, the universe and everything" < 42;}', False)
|
||||
@ -296,7 +325,7 @@ class TestJSInterpreter(unittest.TestCase):
|
||||
def test_undefined(self):
|
||||
self._test('function f() { return undefined === undefined; }', True)
|
||||
self._test('function f() { return undefined; }', JS_Undefined)
|
||||
self._test('function f() {return undefined ?? 42; }', 42)
|
||||
self._test('function f() { return undefined ?? 42; }', 42)
|
||||
self._test('function f() { let v; return v; }', JS_Undefined)
|
||||
self._test('function f() { let v; return v**0; }', 1)
|
||||
self._test('function f() { let v; return [v>42, v<=42, v&&42, 42&&v]; }',
|
||||
|
@ -1,3 +1,4 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
@ -64,6 +65,10 @@ _NaN = float('nan')
|
||||
_Infinity = float('inf')
|
||||
|
||||
|
||||
class JS_Undefined(object):
|
||||
pass
|
||||
|
||||
|
||||
def _js_bit_op(op):
|
||||
|
||||
def zeroise(x):
|
||||
@ -107,12 +112,69 @@ def _js_exp(a, b):
|
||||
return (a or 0) ** b
|
||||
|
||||
|
||||
def _js_eq_op(op):
|
||||
def _js_to_primitive(v):
|
||||
return (
|
||||
','.join(map(_js_toString, v)) if isinstance(v, list)
|
||||
else '[object Object]' if isinstance(v, dict)
|
||||
else compat_str(v) if not isinstance(v, (
|
||||
compat_numeric_types, compat_basestring, bool))
|
||||
else v
|
||||
)
|
||||
|
||||
|
||||
def _js_toString(v):
|
||||
return (
|
||||
'undefined' if v is JS_Undefined
|
||||
else 'Infinity' if v == _Infinity
|
||||
else 'NaN' if v is _NaN
|
||||
else 'null' if v is None
|
||||
else compat_str(v) if isinstance(v, compat_numeric_types)
|
||||
else _js_to_primitive(v))
|
||||
|
||||
|
||||
_nullish = frozenset((None, JS_Undefined))
|
||||
|
||||
|
||||
def _js_eq(a, b):
|
||||
# NaN != any
|
||||
if _NaN in (a, b):
|
||||
return False
|
||||
# Object is Object
|
||||
if isinstance(a, type(b)) and isinstance(b, (dict, list)):
|
||||
return operator.is_(a, b)
|
||||
# general case
|
||||
if a == b:
|
||||
return True
|
||||
# null == undefined
|
||||
a_b = set((a, b))
|
||||
if a_b & _nullish:
|
||||
return a_b <= _nullish
|
||||
a, b = _js_to_primitive(a), _js_to_primitive(b)
|
||||
if not isinstance(a, compat_basestring):
|
||||
a, b = b, a
|
||||
# Number to String: convert the string to a number
|
||||
# Conversion failure results in ... false
|
||||
if isinstance(a, compat_basestring):
|
||||
return float_or_none(a) == b
|
||||
return a == b
|
||||
|
||||
|
||||
def _js_neq(a, b):
|
||||
return not _js_eq(a, b)
|
||||
|
||||
|
||||
def _js_id_op(op):
|
||||
|
||||
@wraps_op(op)
|
||||
def wrapped(a, b):
|
||||
if set((a, b)) <= set((None, JS_Undefined)):
|
||||
return op(a, a)
|
||||
if _NaN in (a, b):
|
||||
return op(_NaN, None)
|
||||
if not isinstance(a, (compat_basestring, compat_numeric_types)):
|
||||
a, b = b, a
|
||||
# strings are === if ==
|
||||
# why 'a' is not 'a': https://stackoverflow.com/a/1504848
|
||||
if isinstance(a, (compat_basestring, compat_numeric_types)):
|
||||
return a == b if op(0, 0) else a != b
|
||||
return op(a, b)
|
||||
|
||||
return wrapped
|
||||
@ -187,10 +249,10 @@ _OPERATORS = (
|
||||
)
|
||||
|
||||
_COMP_OPERATORS = (
|
||||
('===', operator.is_),
|
||||
('!==', operator.is_not),
|
||||
('==', _js_eq_op(operator.eq)),
|
||||
('!=', _js_eq_op(operator.ne)),
|
||||
('===', _js_id_op(operator.is_)),
|
||||
('!==', _js_id_op(operator.is_not)),
|
||||
('==', _js_eq),
|
||||
('!=', _js_neq),
|
||||
('<=', _js_comp_op(operator.le)),
|
||||
('>=', _js_comp_op(operator.ge)),
|
||||
('<', _js_comp_op(operator.lt)),
|
||||
@ -222,10 +284,6 @@ _MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]')))
|
||||
_QUOTES = '\'"/'
|
||||
|
||||
|
||||
class JS_Undefined(object):
|
||||
pass
|
||||
|
||||
|
||||
class JS_Break(ExtractorError):
|
||||
def __init__(self):
|
||||
ExtractorError.__init__(self, 'Invalid break')
|
||||
|
Loading…
Reference in New Issue
Block a user