LibJS: Remove stale FIXME about sloppy-mode this in CallExpression

For non-Reference calls (e.g. (0, fn)(), (cond ? fn : x)()), the
codegen correctly passes undefined as the thisValue, matching step 2b
of EvaluateCall in the spec. OrdinaryCallBindThis then coerces
undefined to the global object in sloppy mode at runtime. Replace the
stale FIXME with a clarifying comment.

Also add comprehensive tests for this-value behavior in non-Reference
call patterns (comma, ternary, logical, assignment, nullish coalescing)
in both sloppy and strict mode.
This commit is contained in:
Andreas Kling 2026-02-08 12:14:57 +01:00 committed by Andreas Kling
parent 8c541380dd
commit 0c843b04e3
2 changed files with 152 additions and 1 deletions

View file

@ -1894,7 +1894,8 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> CallExpression::generat
generator.intern_identifier(identifier.string()));
}
} else {
// FIXME: this = global object in sloppy mode.
// NB: For non-Reference calls, EvaluateCall sets thisValue to undefined.
// OrdinaryCallBindThis coerces undefined to the global object in sloppy mode at runtime.
original_callee = TRY(m_callee->generate_bytecode(generator)).value();
}

View file

@ -410,6 +410,156 @@ describe("with statements", () => {
});
});
describe("non-Reference calls get globalThis in sloppy mode", () => {
// Per EvaluateCall step 2b, when the callee is not a Reference Record,
// thisValue is set to undefined. OrdinaryCallBindThis then coerces
// undefined to the global object in sloppy mode.
function getThis() {
return this;
}
test("comma expression", () => {
expect((0, getThis)()).toBe(globalObject);
});
test("conditional expression (true branch)", () => {
expect((true ? getThis : null)()).toBe(globalObject);
});
test("conditional expression (false branch)", () => {
expect((false ? null : getThis)()).toBe(globalObject);
});
test("logical OR expression", () => {
expect((false || getThis)()).toBe(globalObject);
});
test("logical AND expression", () => {
expect((true && getThis)()).toBe(globalObject);
});
test("nullish coalescing expression", () => {
expect((null ?? getThis)()).toBe(globalObject);
});
test("assignment expression", () => {
var f;
expect((f = getThis)()).toBe(globalObject);
});
test("parenthesized function expression", () => {
expect(
(function () {
return this;
})()
).toBe(globalObject);
});
test("sequence of comma operators", () => {
expect((0, 1, 2, getThis)()).toBe(globalObject);
});
test("function returned from another call", () => {
function returnGetThis() {
return getThis;
}
expect(returnGetThis()()).toBe(globalObject);
});
test("function from array element access", () => {
// NB: Array element access IS a Reference, so this should give the array as `this`.
// This test is here to contrast with non-Reference patterns.
var arr = [getThis];
expect(arr[0]()).toBe(arr);
});
test("function from object property access", () => {
// NB: Property access IS a Reference, so this should give the object as `this`.
var obj = { fn: getThis };
expect(obj.fn()).toBe(obj);
});
test("immediately-invoked function expression", () => {
var result = (function () {
return this;
})();
expect(result).toBe(globalObject);
});
test("nested non-Reference calls", () => {
function outer() {
return (0,
function () {
return this;
})();
}
expect(outer()).toBe(globalObject);
});
test("method extraction via comma loses this binding", () => {
var obj = {
fn: function () {
return this;
},
};
// Direct call: this === obj
expect(obj.fn()).toBe(obj);
// Comma expression strips the Reference: this === globalObject
expect((0, obj.fn)()).toBe(globalObject);
});
test("logical assignment does not affect this", () => {
var f = null;
expect((f ??= getThis)()).toBe(globalObject);
var g = undefined;
expect((g ||= getThis)()).toBe(globalObject);
var h = getThis;
expect((h &&= getThis)()).toBe(globalObject);
});
});
describe("non-Reference calls get undefined this in strict mode", () => {
"use strict";
function getThis() {
return this;
}
test("comma expression", () => {
expect((0, getThis)()).toBeUndefined();
});
test("conditional expression", () => {
expect((true ? getThis : null)()).toBeUndefined();
});
test("logical OR expression", () => {
expect((false || getThis)()).toBeUndefined();
});
test("logical AND expression", () => {
expect((true && getThis)()).toBeUndefined();
});
test("nullish coalescing expression", () => {
expect((null ?? getThis)()).toBeUndefined();
});
test("assignment expression", () => {
var f;
expect((f = getThis)()).toBeUndefined();
});
test("method extraction via comma", () => {
var obj = { fn: getThis };
expect(obj.fn()).toBe(obj);
expect((0, obj.fn)()).toBeUndefined();
});
});
describe("in non strict mode primitive this values are converted to objects", () => {
const array = [true, false];