Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
bcded111d2 fix(test): show empty string keys in object diffs
`Identifier::isEmpty()` returns true for both null identifiers and
empty string `""` identifiers. This caused `forEachPropertyOrdered` and
`forEachPropertyImpl` to skip properties with empty string keys when
iterating over object properties for display.

Replace `property.isEmpty()` with `property.isNull()` to only skip
null identifiers while preserving legitimate empty string property keys.
Also remove redundant `key.len == 0` check in `forEachPropertyImpl`.

Closes #18028

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 10:11:21 +00:00
4 changed files with 94 additions and 116 deletions

View File

@@ -2213,39 +2213,37 @@ pub fn NewParser_(
var is_sloppy_mode_block_level_fn_stmt = false;
const original_member_ref = value.ref;
if (symbol.kind == .hoisted_function) {
if (p.willUseRenamer() and symbol.kind == .hoisted_function) {
// Block-level function declarations behave like "let" in strict mode
if (scope.strict_mode != .sloppy_mode) {
continue;
}
if (p.willUseRenamer()) {
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
const hoisted_ref = p.newSymbol(.hoisted, symbol.original_name) catch unreachable;
symbols = p.symbols.items;
bun.handleOom(scope.generated.append(p.allocator, hoisted_ref));
p.hoisted_ref_for_sloppy_mode_block_fn.put(p.allocator, value.ref, hoisted_ref) catch unreachable;
value.ref = hoisted_ref;
symbol = &symbols[hoisted_ref.innerIndex()];
is_sloppy_mode_block_level_fn_stmt = true;
}
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
const hoisted_ref = p.newSymbol(.hoisted, symbol.original_name) catch unreachable;
symbols = p.symbols.items;
bun.handleOom(scope.generated.append(p.allocator, hoisted_ref));
p.hoisted_ref_for_sloppy_mode_block_fn.put(p.allocator, value.ref, hoisted_ref) catch unreachable;
value.ref = hoisted_ref;
symbol = &symbols[hoisted_ref.innerIndex()];
is_sloppy_mode_block_level_fn_stmt = true;
}
if (hash == null) hash = Scope.getMemberHash(name);

View File

@@ -5138,7 +5138,7 @@ restart:
RETURN_IF_EXCEPTION(scope, void());
for (auto& property : properties) {
if (property.isEmpty() || property.isNull()) [[unlikely]]
if (property.isNull()) [[unlikely]]
continue;
// ignore constructor
@@ -5169,9 +5169,6 @@ restart:
ZigString key = toZigString(property.isSymbol() && !property.isPrivateName() ? property.impl() : property.string());
if (key.len == 0)
continue;
JSC::JSValue propertyValue = jsUndefined();
if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) {
@@ -5312,7 +5309,7 @@ extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJS
auto clientData = WebCore::clientData(vm);
for (auto property : vector) {
if (property.isEmpty() || property.isNull()) [[unlikely]]
if (property.isNull()) [[unlikely]]
continue;
// ignore constructor

View File

@@ -1,83 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("block-scoped function declarations not accessible outside block in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
try { f; console.log("BUG"); } catch(e) { console.log("PASS: " + e.message); }
{ function f() {} }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("PASS:");
expect(stdout).not.toContain("BUG");
expect(exitCode).toBe(0);
});
test("bare reference to block-scoped function throws ReferenceError in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
f;
{ function f() {} }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("ReferenceError");
expect(exitCode).not.toBe(0);
});
test("block-scoped function in ESM (with export) not accessible outside block", async () => {
using dir = tempDir("issue-14715", {
"index.mjs": `try { f; console.log("BUG"); } catch(e) { console.log("PASS: " + e.message); }
{ function f() {} }
export {};`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.mjs"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("PASS:");
expect(stdout).not.toContain("BUG");
expect(exitCode).toBe(0);
});
test("block-scoped function accessible inside block in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
{ function f() { return 42; } console.log("RESULT: " + f()); }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("RESULT: 42");
expect(exitCode).toBe(0);
});

View File

@@ -0,0 +1,66 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/18028
// Object diff in bun test should display empty string keys correctly.
test("toStrictEqual diff shows empty string keys", () => {
expect({
val: { "": "value" },
}).toStrictEqual({
val: { "": "value" },
});
});
test("toEqual diff shows empty string keys", () => {
expect({ "": "hello" }).toEqual({ "": "hello" });
});
test("empty string key with various value types", () => {
expect({ "": 0 }).toEqual({ "": 0 });
expect({ "": null }).toEqual({ "": null });
expect({ "": "" }).toEqual({ "": "" });
expect({ "": false }).toEqual({ "": false });
expect({ "": undefined }).toEqual({ "": undefined });
});
test("empty string key mixed with other keys", () => {
expect({ foo: "bar", "": "value" }).toEqual({ foo: "bar", "": "value" });
});
test("toStrictEqual fails and shows diff with empty string key", async () => {
using dir = tempDir("issue-18028", {
"test.test.ts": `
import { test, expect } from "bun:test";
test("diff", () => {
expect({ val: {} }).toStrictEqual({ val: { "": "value" } });
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "test.test.ts"],
cwd: String(dir),
env: { ...bunEnv, FORCE_COLOR: "0" },
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// The diff section should show non-zero changed lines
expect(stderr).toContain("- Expected - 3");
expect(stderr).toContain("+ Received + 1");
// The diff should include the empty string key
expect(stderr).toContain('"": "value"');
expect(exitCode).toBe(1);
});
test("console.log shows empty string keys", () => {
const result = Bun.spawnSync({
cmd: [bunExe(), "-e", 'console.log({ "": "value", foo: "bar" })'],
env: { ...bunEnv, NO_COLOR: "1" },
});
const stdout = result.stdout.toString();
expect(stdout).toContain('"": "value"');
});