var BUGNUMBER = 1566143; var summary = "Implement the Optional Chain operator (?.) proposal"; print(BUGNUMBER + ": " + summary); // These tests are originally from webkit. // webkit specifics have been removed and error messages have been updated. function shouldBe(actual, expected) { if (actual !== expected) throw new Error(`expected ${expected} but got ${actual}`); } function shouldThrowSyntaxError(script) { let error; try { eval(script); } catch (e) { error = e; } if (!(error instanceof SyntaxError)) throw new Error('Expected SyntaxError!'); } function shouldNotThrowSyntaxError(script) { let error; try { eval(script); } catch (e) { error = e; } if ((error instanceof SyntaxError)) throw new Error('Unxpected SyntaxError!'); } function shouldThrowTypeError(func, messagePrefix) { let error; try { func(); } catch (e) { error = e; } if (!(error instanceof TypeError)) throw new Error('Expected TypeError!'); if (!error.message.includes(messagePrefix)) throw new Error(`TypeError has wrong message!, expected ${messagePrefix} but got ${error.message}`); } function shouldThrowReferenceError(script) { let error; try { eval(script); } catch (e) { error = e; } if (!(error instanceof ReferenceError)) throw new Error('Expected ReferenceError!'); } function testBasicSuccessCases() { shouldBe(undefined?.valueOf(), undefined); shouldBe(null?.valueOf(), undefined); shouldBe(true?.valueOf(), true); shouldBe(false?.valueOf(), false); shouldBe(0?.valueOf(), 0); shouldBe(1?.valueOf(), 1); shouldBe(''?.valueOf(), ''); shouldBe('hi'?.valueOf(), 'hi'); shouldBe(({})?.constructor, Object); shouldBe(({ x: 'hi' })?.x, 'hi'); shouldBe([]?.length, 0); shouldBe(['hi']?.length, 1); shouldBe(undefined?.['valueOf'](), undefined); shouldBe(null?.['valueOf'](), undefined); shouldBe(true?.['valueOf'](), true); shouldBe(false?.['valueOf'](), false); shouldBe(0?.['valueOf'](), 0); shouldBe(1?.['valueOf'](), 1); shouldBe(''?.['valueOf'](), ''); shouldBe('hi'?.['valueOf'](), 'hi'); shouldBe(({})?.['constructor'], Object); shouldBe(({ x: 'hi' })?.['x'], 'hi'); shouldBe([]?.['length'], 0); shouldBe(['hi']?.[0], 'hi'); shouldBe(undefined?.(), undefined); shouldBe(null?.(), undefined); shouldBe((() => 3)?.(), 3); } function testBasicFailureCases() { shouldThrowTypeError(() => true?.(), 'true is not a function'); shouldThrowTypeError(() => false?.(), 'false is not a function'); shouldThrowTypeError(() => 0?.(), '0 is not a function'); shouldThrowTypeError(() => 1?.(), '1 is not a function'); shouldThrowTypeError(() => ''?.(), '"" is not a function'); shouldThrowTypeError(() => 'hi'?.(), '"hi" is not a function'); shouldThrowTypeError(() => ({})?.(), '({}) is not a function'); shouldThrowTypeError(() => ({ x: 'hi' })?.(), '({x:"hi"}) is not a function'); shouldThrowTypeError(() => []?.(), '[] is not a function'); shouldThrowTypeError(() => ['hi']?.(), '[...] is not a function'); } testBasicSuccessCases(); testBasicFailureCases(); shouldThrowTypeError(() => ({})?.i(), '(intermediate value).i is not a function'); shouldBe(({}).i?.(), undefined); shouldBe(({})?.i?.(), undefined); shouldThrowTypeError(() => ({})?.['i'](), '(intermediate value)["i"] is not a function'); shouldBe(({})['i']?.(), undefined); shouldBe(({})?.['i']?.(), undefined); shouldThrowTypeError(() => ({})?.a['b'], '(intermediate value).a is undefined'); shouldBe(({})?.a?.['b'], undefined); shouldBe(null?.a['b']().c, undefined); shouldThrowTypeError(() => ({})?.['a'].b, '(intermediate value)["a"] is undefined'); shouldBe(({})?.['a']?.b, undefined); shouldBe(null?.['a'].b()['c'], undefined); shouldBe(null?.()().a['b'], undefined); const o0 = { a: { b() { return this._b.bind(this); }, _b() { return this.__b; }, __b: { c: 42 } } }; shouldBe(o0?.a?.['b']?.()?.()?.c, 42); shouldBe(o0?.i?.['j']?.()?.()?.k, undefined); shouldBe((o0.a?._b)?.().c, 42); shouldBe((o0.a?._b)().c, 42); shouldBe((o0.a?.b?.())?.().c, 42); shouldBe((o0.a?.['b']?.())?.().c, 42); shouldBe(({ undefined: 3 })?.[null?.a], 3); shouldBe((() => 3)?.(null?.a), 3); const o1 = { count: 0, get x() { this.count++; return () => {}; } }; o1.x?.y; shouldBe(o1.count, 1); o1.x?.['y']; shouldBe(o1.count, 2); o1.x?.(); shouldBe(o1.count, 3); null?.(o1.x); shouldBe(o1.count, 3); shouldBe(delete undefined?.foo, true); shouldBe(delete null?.foo, true); shouldBe(delete undefined?.['foo'], true); shouldBe(delete null?.['foo'], true); shouldBe(delete undefined?.(), true); shouldBe(delete null?.(), true); shouldBe(delete ({}).a?.b?.b, true); shouldBe(delete ({a : {b: undefined}}).a?.b?.b, true); shouldBe(delete ({a : {b: undefined}}).a?.["b"]?.["b"], true); const o2 = { x: 0, y: 0, z() {} }; shouldBe(delete o2?.x, true); shouldBe(o2.x, undefined); shouldBe(o2.y, 0); shouldBe(delete o2?.x, true); shouldBe(delete o2?.['y'], true); shouldBe(o2.y, undefined); shouldBe(delete o2?.['y'], true); shouldBe(delete o2.z?.(), true); function greet(name) { return `hey, ${name}${this.suffix ?? '.'}`; } shouldBe(eval?.('greet("world")'), 'hey, world.'); shouldBe(greet?.call({ suffix: '!' }, 'world'), 'hey, world!'); shouldBe(greet.call?.({ suffix: '!' }, 'world'), 'hey, world!'); shouldBe(null?.call({ suffix: '!' }, 'world'), undefined); shouldBe(({}).call?.({ suffix: '!' }, 'world'), undefined); shouldBe(greet?.apply({ suffix: '?' }, ['world']), 'hey, world?'); shouldBe(greet.apply?.({ suffix: '?' }, ['world']), 'hey, world?'); shouldBe(null?.apply({ suffix: '?' }, ['world']), undefined); shouldBe(({}).apply?.({ suffix: '?' }, ['world']), undefined); shouldThrowSyntaxError('class C {} class D extends C { foo() { return super?.bar; } }'); shouldThrowSyntaxError('class C {} class D extends C { foo() { return super?.["bar"]; } }'); shouldThrowSyntaxError('class C {} class D extends C { constructor() { super?.(); } }'); shouldThrowSyntaxError('const o = { C: class {} }; new o?.C();') shouldThrowSyntaxError('const o = { C: class {} }; new o?.["C"]();') shouldThrowSyntaxError('class C {} new C?.();') shouldThrowSyntaxError('function foo() { new?.target; }'); shouldThrowSyntaxError('function tag() {} tag?.``;'); shouldThrowSyntaxError('const o = { tag() {} }; o?.tag``;'); shouldThrowReferenceError('`${G}`?.r'); // NOT an optional chain shouldBe(false?.4:5, 5); // Special case: binary operators that follow a binary expression shouldThrowReferenceError('(0 || 1 << x)?.$'); shouldThrowReferenceError('(0 || 1 >> x)?.$'); shouldThrowReferenceError('(0 || 1 >>> x)?.$'); shouldThrowReferenceError('(0 || 1 + x)?.$'); shouldThrowReferenceError('(0 || 1 - x)?.$'); shouldThrowReferenceError('(0 || 1 % x)?.$'); shouldThrowReferenceError('(0 || 1 / x)?.$'); shouldThrowReferenceError('(0 || 1 == x)?.$'); shouldThrowReferenceError('(0 || 1 != x)?.$'); shouldThrowReferenceError('(0 || 1 !== x)?.$'); shouldThrowReferenceError('(0 || 1 === x)?.$'); shouldThrowReferenceError('(0 || 1 <= x)?.$'); shouldThrowReferenceError('(0 || 1 >= x)?.$'); shouldThrowReferenceError('(0 || 1 ** x)?.$'); shouldThrowReferenceError('(0 || 1 | x)?.$'); shouldThrowReferenceError('(0 || 1 & x)?.$'); shouldThrowReferenceError('(0 || 1 instanceof x)?.$'); shouldThrowReferenceError('(0 || "foo" in x)?.$'); function testSideEffectCountFunction() { let count = 0; let a = { b: { c: { d: () => { count++; return a; } } } } a.b.c.d?.()?.b?.c?.d shouldBe(count, 1); } function testSideEffectCountGetters() { let count = 0; let a = { get b() { count++; return { c: {} }; } } a.b?.c?.d; shouldBe(count, 1); a.b?.c?.d; shouldBe(count, 2); } testSideEffectCountFunction(); testSideEffectCountGetters(); // stress test SM shouldBe(({a : {b: undefined}}).a.b?.()()(), undefined); shouldBe(({a : {b: undefined}}).a.b?.()?.()(), undefined); shouldBe(({a : {b: () => undefined}}).a.b?.()?.(), undefined); shouldThrowTypeError(() => delete ({a : {b: undefined}}).a?.b.b.c, '(intermediate value).a.b is undefined'); shouldBe(delete ({a : {b: undefined}}).a?.["b"]?.["b"], true); shouldBe(delete undefined ?.x[y+1], true); shouldThrowTypeError(() => (({a : {b: () => undefined}}).a.b?.())(), 'undefined is not a function'); shouldThrowTypeError(() => (delete[1]?.r[delete[1]?.r1]), "[...].r is undefined"); shouldThrowTypeError(() => (delete[1]?.r[[1]?.r1]), "[...].r is undefined"); if (typeof reportCompare === "function") reportCompare(true, true); print("Tests complete");