Files
bun.sh/test/bundler/bundler_minify_symbol_for.test.ts
Jarred Sumner 9e4700ee2d Remove unused Symbol.for(primitive) calls in bundler (#20888)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
2025-07-12 00:52:07 -07:00

341 lines
11 KiB
TypeScript

import { describe, expect } from "bun:test";
import { itBundled } from "./expectBundled";
describe("bundler", () => {
describe("minify/Symbol.for", () => {
// Test basic Symbol.for removal when unused
itBundled("minify/SymbolForUnused", {
files: {
"/entry.js": /* js */ `
// These should be removed when minifySyntax is true
Symbol.for("test1");
Symbol.for("test2");
Symbol.for(\`test3\`);
Symbol.for("test" + 4); // This has a side effect (string concatenation)
// Keep reference to prove concatenation happened
var sideEffect = "test" + 4;
Symbol.for(sideEffect);
// These should NOT be removed (used values)
const s1 = Symbol.for("used1");
let s2 = Symbol.for("used2");
var s3 = Symbol.for("used3");
// Function argument - should not be removed
console.log(Symbol.for("argument"));
// Property access - should not be removed
const obj = { prop: Symbol.for("property") };
// Return value - should not be removed
function getSymbol() {
return Symbol.for("return");
}
capture(s1, s2, s3, obj.prop, getSymbol(), sideEffect);
`,
},
minifySyntax: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should remove unused Symbol.for calls
expect(output).not.toContain('Symbol.for("test1")');
expect(output).not.toContain('Symbol.for("test2")');
expect(output).not.toContain("Symbol.for(`test3`)");
// Should keep the concatenation because sideEffect variable is used
expect(output).toContain("test4");
// Should keep used Symbol.for calls
expect(output).toContain('Symbol.for("used1")');
expect(output).toContain('Symbol.for("used2")');
expect(output).toContain('Symbol.for("used3")');
expect(output).toContain('Symbol.for("argument")');
expect(output).toContain('Symbol.for("property")');
expect(output).toContain('Symbol.for("return")');
},
});
// Test that Symbol.for is not removed when minifySyntax is false
itBundled("minify/SymbolForNoMinifySyntax", {
files: {
"/entry.js": /* js */ `
Symbol.for("test1");
Symbol.for("test2");
const s = Symbol.for("test3");
capture(s);
`,
},
minifySyntax: false,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should keep all Symbol.for calls when minifySyntax is false
expect(output).toContain('Symbol.for("test1")');
expect(output).toContain('Symbol.for("test2")');
expect(output).toContain('Symbol.for("test3")');
},
});
// Test interaction with other minification options
itBundled("minify/SymbolForWithWhitespace", {
files: {
"/entry.js": /* js */ `
// Unused calls should be removed
Symbol.for("remove-me-1");
Symbol.for("remove-me-2");
// Used call should remain
const sym = Symbol.for("keep-me");
// Test with complex expressions
const ab = "a" + "b";
Symbol.for(ab); // Keep the variable to ensure concatenation happens
Symbol.for(\`template\`);
capture(sym, ab);
`,
},
minifySyntax: true,
minifyWhitespace: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should remove unused calls
expect(output).not.toContain("remove-me-1");
expect(output).not.toContain("remove-me-2");
// Should keep used call
expect(output).toContain('Symbol.for("keep-me")');
// Should keep side effect (the concatenation is kept because ab is used)
expect(output).toContain("ab");
},
});
// Test edge cases
itBundled("minify/SymbolForEdgeCases", {
files: {
"/entry.js": /* js */ `
// Optional chaining - these are preserved because optional chaining has observable behavior
Symbol?.for("optional1");
Symbol?.for?.("optional2");
// In conditional - these should be optimized based on the condition
true && Symbol.for("conditional1");
false || Symbol.for("conditional2");
// In ternary - these should be optimized based on the condition
true ? Symbol.for("ternary1") : null;
false ? null : Symbol.for("ternary2");
// Nested calls
Symbol.for(Symbol.for("nested"));
// With spread
const arr = [...[Symbol.for("spread")]];
// Property key
const obj = {
[Symbol.for("key")]: "value"
};
capture(arr, obj);
`,
},
minifySyntax: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Optional chaining preserves the call because it has observable behavior (checking if Symbol exists)
expect(output).toContain("optional1");
expect(output).toContain("optional2");
// All the conditional/ternary expressions were optimized away completely
// because they evaluate to unused Symbol.for calls
expect(output).not.toContain("conditional1");
expect(output).not.toContain("conditional2");
expect(output).not.toContain("ternary1");
expect(output).not.toContain("ternary2");
// Nested call was also optimized away
expect(output).not.toContain("nested");
// Used in spread - should keep
expect(output).toContain('Symbol.for("spread")');
// Used as property key - should keep
expect(output).toContain('Symbol.for("key")');
},
});
// Test that Symbol.keyFor is not affected (it's still in the property access list)
itBundled("minify/SymbolKeyForNotAffected", {
files: {
"/entry.js": /* js */ `
// Symbol.keyFor should still be removed as a property access
Symbol.keyFor;
// But not when called
const sym = Symbol.for("test");
const key = Symbol.keyFor(sym);
capture(key);
`,
},
minifySyntax: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// The unused property access "Symbol.keyFor;" should be removed
// But the function call "Symbol.keyFor(sym)" should remain
// So we should find exactly one occurrence of "Symbol.keyFor"
const keyForMatches = output.match(/Symbol\.keyFor/g) || [];
expect(keyForMatches.length).toBe(1);
// Function call should remain
expect(output).toContain("Symbol.keyFor(");
},
});
// Test interaction with production mode
itBundled("minify/SymbolForProduction", {
files: {
"/entry.js": /* js */ `
// Unused
Symbol.for("remove-in-prod");
// Used
const s = Symbol.for("keep-in-prod");
// Side effects
Symbol.for(someGlobal);
capture(s);
`,
},
production: true, // This enables minifySyntax
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should remove unused
expect(output).not.toContain("remove-in-prod");
// Should keep used
expect(output).toContain("keep-in-prod");
// Should keep side effects
expect(output).toContain("someGlobal");
},
});
// Test with bundling disabled (transform mode)
itBundled("minify/SymbolForTransformMode", {
files: {
"/entry.js": /* js */ `
Symbol.for("unused");
const used = Symbol.for("used");
export { used };
`,
},
bundling: false,
minifySyntax: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should remove unused in transform mode too
expect(output).not.toContain('"unused"');
// Should keep used
expect(output).toContain('Symbol.for("used")');
},
});
// Test interaction with tree shaking
itBundled("minify/SymbolForTreeShaking", {
files: {
"/entry.js": /* js */ `
import { sym } from "./lib.js";
// This should be removed
Symbol.for("entry-unused");
capture(sym);
`,
"/lib.js": /* js */ `
// This should be removed (unused export)
export const unused = Symbol.for("lib-unused-export");
// This should be kept (used export)
export const sym = Symbol.for("lib-used-export");
// This should be removed (not exported)
Symbol.for("lib-internal");
`,
},
minifySyntax: true,
treeShaking: true,
onAfterBundle(api) {
const output = api.readFile("/out.js");
// Should remove all unused Symbol.for calls
expect(output).not.toContain("entry-unused");
expect(output).not.toContain("lib-unused-export");
expect(output).not.toContain("lib-internal");
// Should keep used Symbol.for call
expect(output).toContain("lib-used-export");
},
});
// Test that Symbol.for is still called at runtime when overridden
itBundled("minify/SymbolForRuntimeOverride", {
files: {
"/entry.js": /* js */ `
let callCount = 0;
const originalSymbolFor = Symbol.for;
// Override Symbol.for to count calls
Symbol.for = function(key) {
callCount++;
return originalSymbolFor.call(this, key);
};
// These unused calls should be removed at bundle time
Symbol.for("unused1");
Symbol.for("unused2");
// These used calls should remain and increment callCount
const s1 = Symbol.for("used1");
const s2 = Symbol.for("used2");
// Restore original
Symbol.for = originalSymbolFor;
// Verify that Symbol.for was called for the used symbols
if (callCount !== 2) {
throw new Error(\`Expected 2 calls to Symbol.for, got \${callCount}\`);
}
// Verify the symbols work correctly
if (s1 !== Symbol.for("used1")) {
throw new Error("Symbol s1 mismatch");
}
if (s2 !== Symbol.for("used2")) {
throw new Error("Symbol s2 mismatch");
}
console.log("PASS");
`,
},
minifySyntax: true,
run: {
stdout: "PASS",
},
});
});
});