LibJS: Preserve this binding for tagged with identifiers
Route tagged template identifier lookup through GetCalleeAndThisFromEnvironment only when the identifier is non-local. Keep local and global identifiers on Identifier::generate_bytecode so TDZ checks and ordinary undefined-this behavior stay intact. Expand runtime coverage with a tagged-template TDZ regression case, sequential with-binding calls, and getter-returned tag functions.
This commit is contained in:
parent
9f436356d0
commit
3eb03b4817
2 changed files with 446 additions and 0 deletions
|
|
@ -2657,6 +2657,25 @@ Bytecode::CodeGenerationErrorOr<Optional<ScopedOperand>> TaggedTemplateLiteral::
|
|||
return TagAndThisValue { .tag = base_and_value.value, .this_value = base_and_value.base };
|
||||
}
|
||||
|
||||
if (is<Identifier>(*m_tag)) {
|
||||
auto& identifier = static_cast<Identifier const&>(*m_tag);
|
||||
if (identifier.is_local() || identifier.is_global()) {
|
||||
// Keep the normal Identifier path so local/global tags preserve
|
||||
// TDZ behavior; only non-local identifiers need with-aware
|
||||
// callee/this extraction.
|
||||
auto tag = TRY(m_tag->generate_bytecode(generator)).value();
|
||||
return TagAndThisValue { .tag = tag, .this_value = generator.add_constant(js_undefined()) };
|
||||
}
|
||||
|
||||
auto tag = generator.allocate_register();
|
||||
auto this_value = generator.allocate_register();
|
||||
generator.emit<Bytecode::Op::GetCalleeAndThisFromEnvironment>(
|
||||
tag,
|
||||
this_value,
|
||||
generator.intern_identifier(identifier.string()));
|
||||
return TagAndThisValue { .tag = tag, .this_value = this_value };
|
||||
}
|
||||
|
||||
auto tag = TRY(m_tag->generate_bytecode(generator)).value();
|
||||
return TagAndThisValue { .tag = tag, .this_value = generator.add_constant(js_undefined()) };
|
||||
}());
|
||||
|
|
|
|||
427
Tests/LibJS/Runtime/tagged-template-literal-with-this.js
Normal file
427
Tests/LibJS/Runtime/tagged-template-literal-with-this.js
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
describe("tagged template literal this value in with statements", () => {
|
||||
test("this value should be the binding object when tag is resolved via with", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via nested with", () => {
|
||||
let capturedThis;
|
||||
const obj1 = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
const obj2 = { obj: obj1 };
|
||||
|
||||
with (obj2) {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj1);
|
||||
});
|
||||
|
||||
test("this value should be globalThis when tag is a local variable", () => {
|
||||
let capturedThis;
|
||||
function tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
}
|
||||
|
||||
const obj = { foo: 42 };
|
||||
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(globalThis);
|
||||
});
|
||||
|
||||
test("local identifier tag throws ReferenceError when in TDZ", () => {
|
||||
expect(() => {
|
||||
(function (a = tag`test`, tag = () => {}) {})();
|
||||
}).toThrowWithMessage(ReferenceError, "Binding <empty> is not initialized");
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via with with expressions", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings, ...values) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
with (obj) {
|
||||
tag`test${42}`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via with with multiple expressions", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings, ...values) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
with (obj) {
|
||||
tag`a${1}b${2}c`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via with in function", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
function test() {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via with in arrow function", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
const test = () => {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
};
|
||||
|
||||
test();
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is resolved via with with proxy", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
const proxy = new Proxy(obj, {});
|
||||
|
||||
with (proxy) {
|
||||
tag`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(proxy);
|
||||
});
|
||||
|
||||
test("this value should be the object when tag is a member expression inside with", () => {
|
||||
let capturedThis;
|
||||
const obj1 = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
const obj2 = { obj: obj1 };
|
||||
|
||||
with (obj2) {
|
||||
obj.tag`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj1);
|
||||
});
|
||||
|
||||
test("this value should be the object when tag is a computed member expression inside with", () => {
|
||||
let capturedThis;
|
||||
const obj1 = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
const obj2 = { obj: obj1 };
|
||||
|
||||
with (obj2) {
|
||||
obj["tag"]`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj1);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag shadowing occurs", () => {
|
||||
let capturedThis;
|
||||
const obj1 = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
const obj2 = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
with (obj1) {
|
||||
with (obj2) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj2);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in try-catch", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in try-finally", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
} finally {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in for loop", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < 1; i++) {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in while loop", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < 1) {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in do-while loop", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
do {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
i++;
|
||||
} while (i < 1);
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in if statement", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
if (true) {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in switch statement", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
switch (1) {
|
||||
case 1:
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in eval", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
function tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
}
|
||||
|
||||
eval("with (obj) { tag`test`; }");
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value should be the binding object when tag is from with in nested function", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
tag(strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
function outer() {
|
||||
function inner() {
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
}
|
||||
inner();
|
||||
}
|
||||
|
||||
outer();
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
|
||||
test("this value tracks each with binding even when reused in a row", () => {
|
||||
const seen = [];
|
||||
const obj1 = {
|
||||
tag(strings) {
|
||||
seen.push(this);
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
const obj2 = {
|
||||
tag(strings) {
|
||||
seen.push(this);
|
||||
return strings;
|
||||
},
|
||||
};
|
||||
|
||||
with (obj1) {
|
||||
tag`first`;
|
||||
}
|
||||
with (obj2) {
|
||||
tag`second`;
|
||||
}
|
||||
|
||||
expect(seen).toEqual([obj1, obj2]);
|
||||
});
|
||||
|
||||
test("since getter returns functions per call the binding object still becomes this", () => {
|
||||
let capturedThis;
|
||||
const obj = {
|
||||
get tag() {
|
||||
return function (strings) {
|
||||
capturedThis = this;
|
||||
return strings;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
with (obj) {
|
||||
tag`test`;
|
||||
}
|
||||
|
||||
expect(capturedThis).toBe(obj);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue