Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4222,6 +4222,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
detail: &[u8],
) -> Result<(), crate::Error> {
let text: &'a [u8] = match feature {
StrictModeFeature::DeleteBareName => b"\"delete\" of a bare identifier",
StrictModeFeature::EvalOrArguments => bun_alloc::arena_format!(
in self.arena,
"Declarations with the name \"{}\"",
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,7 @@ pub struct ParsedPath<'a> {

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StrictModeFeature {
DeleteBareName,
EvalOrArguments,
ReservedWord,
}
Expand Down
8 changes: 8 additions & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
Op::UnDelete => {
if matches!(e_.value.data, Data::EIdentifier(..)) {
p.mark_strict_mode_feature(
StrictModeFeature::DeleteBareName,
js_lexer::range_of_identifier(p.source, e_.value.loc),
b"",
)
.expect("unreachable");
}
p.visit_expr_in_out(&mut e_.value, ExprIn::default());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
_ => {
Expand Down
97 changes: 97 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3275,6 +3275,103 @@ describe("bundler", () => {
api.expectFile("/out.js").toContain("var arguments = 1;");
},
});
// `delete x` on a bare identifier is an early SyntaxError in strict mode, and
// every ES module is strict. Report it where the source is, like esbuild does,
// instead of emitting output that fails to parse.
itBundled("edgecase/DeleteBareIdentifierInESModuleIsAnError", {
files: {
"/entry.js": /* js */ `
import { y } from "./y.js";
var x = 1;
delete x;
console.log(x, y);
`,
"/y.js": /* js */ `
export const y = 2;
`,
},
bundleErrors: {
"/entry.js": ['"delete" of a bare identifier cannot be used in strict mode'],
},
});
itBundled("edgecase/DeleteBareIdentifierInUseStrictFileIsAnError", {
files: {
"/entry.js": /* js */ `
require("./lib.cjs");
`,
"/lib.cjs": /* js */ `
"use strict";
var x = 1;
delete x;
`,
},
format: "cjs",
bundleErrors: {
"/lib.cjs": ['"delete" of a bare identifier cannot be used in strict mode'],
},
});
itBundled("edgecase/SloppyDeleteBareIdentifierESMOutputIsAnError", {
files: {
"/entry.js": /* js */ `
import "./lib.cjs";
`,
"/lib.cjs": /* js */ `
var x = 1;
delete x;
console.log("del", typeof x);
`,
},
format: "esm",
bundleErrors: {
"/lib.cjs": ['"delete" of a bare identifier cannot be used with the ESM output format due to strict mode'],
},
});
// The cjs and iife output formats are sloppy, so the statement stays as written.
// Node runs the output: bun loads a script with no CommonJS marker as an ES module.
itBundled("edgecase/SloppyDeleteBareIdentifierCJSOutput", {
files: {
"/entry.js": /* js */ `
var x = 1;
delete x;
console.log("del", typeof x);
`,
},
format: "cjs",
onAfterBundle(api) {
api.expectFile("/out.js").toContain("delete x;");
},
run: { runtime: "node", stdout: "del number" },
});
itBundled("edgecase/SloppyDeleteBareIdentifierIIFEOutput", {
files: {
"/entry.js": /* js */ `
var x = 1;
delete x;
console.log("del", typeof x);
`,
},
format: "iife",
onAfterBundle(api) {
api.expectFile("/out.js").toContain("delete x;");
},
run: { runtime: "node", stdout: "del number" },
});
// Only a bare identifier is forbidden. A property access, a call, or a
// parenthesized comma expression is a value and stays valid in strict mode.
itBundled("edgecase/DeleteNonIdentifierOperandsESMOutput", {
files: {
"/entry.js": /* js */ `
import "./lib.cjs";
`,
"/lib.cjs": /* js */ `
var o = { x: 1, y: 2 };
var z = 3;
console.log(delete o.x, delete o["y"], delete (0, z), delete (1 ? z : z), delete o?.x, typeof z);
`,
},
format: "esm",
run: { stdout: "true true true true true number" },
});
});

for (const backend of ["api", "cli"] as const) {
Expand Down
10 changes: 6 additions & 4 deletions test/bundler/esbuild/lower.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,10 +1528,12 @@ describe.todo("bundler", () => {
"/delete-3.js": `delete (1 ? z : z)`,
},
format: "esm",
/* TODO FIX expectedScanLog: `delete-1.js: ERROR: Delete of a bare identifier cannot be used with the "esm" output format due to strict mode
delete-2.js: ERROR: Delete of a bare identifier cannot be used with the "esm" output format due to strict mode
with.js: ERROR: With statements cannot be used with the "esm" output format due to strict mode
`, */
// esbuild also reports `with.js: With statements cannot be used with the "esm"
// output format due to strict mode`. Bun has no strict-mode check for `with`.
bundleErrors: {
"/delete-1.js": ['"delete" of a bare identifier cannot be used with the ESM output format due to strict mode'],
"/delete-2.js": ['"delete" of a bare identifier cannot be used with the ESM output format due to strict mode'],
},
});
itBundled("lower/LowerPrivateClassFieldOrder", {
// GENERATED
Expand Down
37 changes: 31 additions & 6 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2676,7 +2676,9 @@ console.log(<div {...obj} key="after" />);`),
});

it("exponentiation", () => {
expectPrinted("(delete x) ** 0", "(delete x) ** 0");
// `expectPrinted` wraps the input in `export default (...)`, which is strict
// mode, where `delete x` is an error. Use the plain helper for that case.
expectPrinted_("(delete x) ** 0", "(delete x) ** 0");
expectPrinted("(delete x.prop) ** 0", "(delete x.prop) ** 0");
expectPrinted("(delete x[0]) ** 0", "(delete x[0]) ** 0");

Expand Down Expand Up @@ -2705,7 +2707,7 @@ console.log(<div {...obj} key="after" />);`),
expectPrinted("(~1) ** 2", "(~1) ** 2");
expectPrinted("(!1) ** 2", "false ** 2");
expectPrinted("(void x) ** 2", "(void x) ** 2");
expectPrinted("(delete x) ** 2", "(delete x) ** 2");
expectPrinted_("(delete x) ** 2", "(delete x) ** 2");
expectPrinted("(typeof x) ** 2", "(typeof x) ** 2");
expectPrinted("undefined ** 2", "undefined ** 2");

Expand Down Expand Up @@ -2785,10 +2787,8 @@ console.log(<div {...obj} key="after" />);`),

expectPrinted_("async function f() { await delete x }", "async function f() {\n await delete x;\n}");

// expectParseError(
// "await delete x",
// "Delete of a bare identifier cannot be used in an ECMAScript module"
// );
// Top-level await makes the file a module, so this is strict mode.
expectParseError("await delete x", '"delete" of a bare identifier cannot be used in strict mode');
});

it("import assert", () => {
Expand Down Expand Up @@ -3557,6 +3557,31 @@ class Foo {
);
});

it("delete of a bare identifier in strict mode", () => {
const error = '"delete" of a bare identifier cannot be used in strict mode';
expectParseError('"use strict"; delete x', error);
expectParseError('"use strict"; delete (x)', error);
expectParseError('"use strict"; delete ((x))', error);
expectParseError("delete x; export {}", error);
expectParseError("import './a'; delete x", error);
expectParseError("await 1; delete x", error);
expectParseError("class A { m() { delete x } }", error);
expectParseError("function f() { 'use strict'; delete x }", error);

// Property access and other operands stay valid in strict mode. A parenthesized
// comma or conditional expression is a value, not a reference, so it is not a
// bare identifier even when it folds to one.
expectPrinted_(
"'use strict'; delete x.y; delete x[0]; delete x?.y; delete (0, x); delete (1 ? x : x); delete f()",
"delete x.y;\ndelete x[0];\ndelete x?.y;\ndelete (0, x);\ndelete (0, x);\ndelete f()",
);

// Sloppy mode allows it when the transpiler is not bundling.
expectPrinted_("var x = 1; delete x", "var x = 1;\ndelete x");
expectPrinted_("delete (x)", "delete x");
expectPrinted_("function f() { delete x }", "function f() {\n delete x;\n}");
});

describe("simplification", () => {
const transpiler = new Bun.Transpiler({
loader: "tsx",
Expand Down
Loading