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
16 changes: 6 additions & 10 deletions src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1342,15 +1342,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.lexer.expect(T::TStringLiteral)?;
}

if !p.lexer.has_newline_before
&& (
// Import Assertions are deprecated.
// Import Attributes are the new way to do this.
// But some code may still use "assert"
// We support both and treat them identically.
// Once Prettier & TypeScript support import attributes, we will add runtime support
p.lexer.is_contextual_keyword(b"assert") || p.lexer.token == T::TWith
)
// Only the deprecated "assert" form has a [no LineTerminator here] restriction.
if p.lexer.token == T::TWith
|| (!p.lexer.has_newline_before && p.lexer.is_contextual_keyword(b"assert"))
{
p.lexer.next()?;
p.lexer.expect(T::TOpenBrace)?;
Expand Down Expand Up @@ -1622,7 +1616,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// In TypeScript, "async <ident>" not followed by "=>" treats "async" as
// a plain identifier (e.g. "async as T"), matching tsc's two-token
// lookahead in isUnParenthesizedAsyncArrowFunctionWorker (TypeScript#8444).
let is_arrow_fn = !Self::IS_TYPESCRIPT_ENABLED
// "async of" needs the same lookahead: "for await (async of xs)".
let is_arrow_fn = (!Self::IS_TYPESCRIPT_ENABLED
&& !p.lexer.is_contextual_keyword(b"of"))
|| p.check_for_arrow_after_the_current_token();

if is_arrow_fn {
Expand Down
30 changes: 29 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,8 @@ pub(crate) mod __gated_printer {
pub(crate) export_default_start: i32,
pub(crate) arrow_expr_start: i32,
pub(crate) for_of_init_start: i32,
/// Start of the expression in a for, for-in, or for-of head.
pub(crate) for_init_start: i32,
pub(crate) prev_op: Op::Code,
pub(crate) prev_op_end: i32,
pub(crate) prev_num_end: i32,
Expand Down Expand Up @@ -3515,11 +3517,16 @@ pub(crate) mod __gated_printer {
}

if has_pure_comment {
let was_stmt_start = self.stmt_start == self.writer.written();
let n = self.writer.written();
let was_stmt_start = self.stmt_start == n;
let was_for_init_start = self.for_init_start == n;
self.print_pure();
if was_stmt_start {
self.stmt_start = self.writer.written();
}
if was_for_init_start {
self.for_init_start = self.writer.written();
}
}
// We only want to generate an unbound eval() in CommonJS
self.call_target = Some(e.target.data);
Expand Down Expand Up @@ -3774,7 +3781,26 @@ pub(crate) mod __gated_printer {

// The index target is not directly followed by `of`.
flags.remove(ExprFlag::IsFollowedByOf);

// A statement or for loop head must not start with "let [".
let wrap_let = {
let n = self.writer.written();
(n == self.stmt_start || n == self.for_init_start)
&& !matches!(e.index.data, ExprData::EPrivateIdentifier(_))
&& match &e.target.data {
ExprData::EIdentifier(id) => {
self.name_for_symbol(id.ref_) == b"let"
}
_ => false,
}
};
if wrap_let {
self.print(b"(");
}
self.print_expr(e.target, Level::Postfix, flags);
if wrap_let {
self.print(b")");
}

let is_optional_chain_start =
e.optional_chain == Some(js_ast::OptionalChain::Start);
Expand Down Expand Up @@ -6538,6 +6564,7 @@ pub(crate) mod __gated_printer {
pub(crate) fn print_for_loop_init(&mut self, init_st: Stmt, extra_flags: ExprFlagSet) {
match &init_st.data {
StmtData::SExpr(s) => {
self.for_init_start = self.writer.written();
self.print_expr(
s.value,
Level::Lowest,
Expand Down Expand Up @@ -7021,6 +7048,7 @@ pub(crate) mod __gated_printer {
export_default_start: -1,
arrow_expr_start: -1,
for_of_init_start: -1,
for_init_start: -1,
prev_op: Op::Code::BinAdd,
prev_op_end: -1,
prev_num_end: -1,
Expand Down
17 changes: 17 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3515,6 +3515,23 @@ describe("bundler", () => {
`);
},
});
itBundled("edgecase/LetIndexAfterPureCommentKeepsParens", {
files: {
"/entry.cjs": `
globalThis.let = [() => 1];
for (/* @__PURE__ */ (let)[0]();;) break;
console.log(typeof let);
`,
},
outfile: "out.cjs",
target: "node",
format: "cjs",
emitDCEAnnotations: true,
onAfterBundle(api) {
api.expectFile("out.cjs").toContain("for (/* @__PURE__ */ (let)[0]();;");
},
run: { stdout: "object" },
});
itBundled("edgecase/NonAsciiIdentifierPreserved", {
files: {
"/entry.js": /* js */ `
Expand Down
46 changes: 45 additions & 1 deletion test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3037,12 +3037,46 @@ console.log(<div {...obj} key="after" />);`),
expectPrinted_("for ((let) of [7]);", "for ((let) of [7])\n ;\n");

// The keyword spelling is a syntax error, which is why the parentheses matter
expect(() => parsed("for (async of [7]);", false, false)).toThrow();
expectParseError("for (async of [7]);", 'For loop initializers cannot start with "async of"');
expectParseError("for (async\nof [7]);", 'For loop initializers cannot start with "async of"');
expectPrinted_(
"async function f() { for await (async\nof [7]); }",
"async function f() {\n for await ((async) of [7])\n ;\n}",
);

// "for await (async of" has no such restriction: "async" is a plain identifier there
expectPrinted_(
"async function f() { for await (async of [7]); }",
"async function f() {\n for await ((async) of [7])\n ;\n}",
);
expectPrinted_("for (async of => {};;);", "for (async (of) => {};; )\n ;\n");
expectPrinted_("x = async of => of", "x = async (of) => of");
expectParseError("async of", 'Expected ";" but found "of"');
});

it("let [ at the start of a statement keeps its parentheses", () => {
// An expression statement and a for loop head must not start with "let [".
expectPrinted_("var let = []; (let)[0] = 1;", "var let = [];\n(let)[0] = 1;\n");
expectPrinted_("if (1) (let)[0] = 2;", "if (1)\n (let)[0] = 2;\n");
expectPrinted_("(let)[0] = 1;", "(let)[0] = 1;\n");
expectPrinted_("x;\n(let)[0] = 1;", "x;\n(let)[0] = 1;\n");
// Without the parentheses this one still parses, as a destructuring declaration
expectPrinted_("(let)\n[a] = b;", "(let)[a] = b;\n");
expectPrinted_("x;\n(let)\n[a] = b;", "x;\n(let)[a] = b;\n");
expectPrinted_("(let)[0].x = 1;", "(let)[0].x = 1;\n");
expectPrinted_("(let)[0]();", "(let)[0]();\n");
expectPrinted_("(let)[0]++;", "(let)[0]++;\n");
expectPrinted_("for ((let)[0] in x);", "for ((let)[0] in x)\n ;\n");
expectPrinted_("for ((let)[0] of x);", "for ((let)[0] of x)\n ;\n");
expectPrinted_("for ((let)[0];;);", "for ((let)[0];; )\n ;\n");

// No parentheses are needed anywhere else
expectPrinted_("var let = {}; (let).x = 1;", "var let = {};\nlet.x = 1;\n");
expectPrinted_("x = (let)[0];", "x = let[0];\n");
expectPrinted_("x, (let)[0] = 1;", "x, let[0] = 1;\n");
expectPrinted_("for (x; (let)[0];);", "for (x;let[0]; )\n ;\n");
expectPrinted_("for (x in (let)[0]);", "for (x in let[0])\n ;\n");
expectPrinted_("(lett)[0] = 1;", "lett[0] = 1;\n");
});

it("first token of the output", async () => {
Expand Down Expand Up @@ -3113,6 +3147,16 @@ console.log(<div {...obj} key="after" />);`),
expectPrinted_(`import("./foo.json", { type: "json" });`, `import("./foo.json", { type: \"json\" })`);
});

it("import attributes on the next line", () => {
// "with" has no [no LineTerminator here] restriction, unlike the legacy "assert"
expectPrinted_(`import json from "./foo.json"\nwith { type: "json" };`, `import json from "./foo.json"`);
expectPrinted_(`import json from "./foo.json"\n with { type: "json" };`, `import json from "./foo.json"`);
expectPrinted_(`import "./foo.json"\nwith { type: "json" };`, `import"./foo.json"`);
expectPrinted_(`export { a } from "./foo.json"\nwith { type: "json" };`, `export { a } from "./foo.json"`);
expectPrinted_(`export * from "./foo.json"\nwith { type: "json" };`, `export * from "./foo.json"`);
expect(() => parsed(`import json from "./foo.json"\nassert { type: "json" };`, false, false)).toThrow();
});

it("import with unicode", () => {
expectPrinted_(`import { name } from 'modထ';`, `import { name } from "modထ"`);
expectPrinted_(`import { name } from 'mod\\u1011';`, `import { name } from "modထ"`);
Expand Down
Loading