mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(codegen): wrap string literal in parens after directives (#25435)
When printing statements (`Codegen::print_directives_and_statements`), if first statement is an `ExpressionStatement` containing just a `StringLiteral`, it has to be parenthesized to avoid it becoming a directive.
Fix 4 different bugs in this logic.
## 1\. Only protected when there were no directives at all
The check was gated on `directives.is_empty()`, but the statement at risk is the first non-directive one, whatever its index. The second statement below printed bare, and re-parsing turned it into a directive - switching the program from sloppy to strict mode.
```js
// Input
"use server";
("use strict");
// Output, before
"use server";
"use strict"; // Now a directive - program is in strict mode
// Output, after this PR
"use server";
("use strict");
```
## 2\. No indent on the protected path
The path printed the statement itself instead of going through `ExpressionStatement`'s printer, and never printed the indent.
```js
// Output, before
function f() {
("x");
}
// Output, after this PR
function f() {
("x");
}
```
## 3\. Leading comments dropped on the protected path
Same cause - the statement's own printer is what prints its comments.
```js
// Input
// keep me
("use strict");
// Output, before
("use strict");
// Output, after this PR
// keep me
("use strict");
```
## 4\. Unprotected in minify mode
Protection was skipped entirely when minifying. A string usually minifies to a template literal, which cannot be a directive - but only where that is the shortest form. One backtick or `${` makes quotes shorter, and then it's printed as a directive.
```js
// Input
("`");
// Output, before
"`"; // Now a directive
// Output, after this PR
`\``;
```
Minified output is now always a template literal:
```js
("`"); // -> `\``;
("${}"); // -> `\${}`;
```
A template is never a directive, costs no more than `("...")`, and survives a round trip.
In the common case (no backticks or `${` in the string), printing as a template literal (`` `use strict`; ``) is more compact than a parenthesized quoted string (` ("use strict"); `).
Note: this visibly changes minified output for this construct - a template literal where there was a parenthesized string. Behavior is unchanged.
This commit is contained in:
1 parent
d0b8d6ff22
commit
bbd6ed38df
4 files changed
+112
-15
No files matched your search
@@ -697,6 +697,7 @@ impl<'a> Codegen<'a> {
|
||||
for directive in directives {
|
||||
directive.print(self, ctx);
|
||||
}
|
||||
|
||||
let Some((first, rest)) = stmts.split_first() else {
|
||||
self.print_orphan_comments_before(scope_end);
|
||||
return;
|
||||
@@ -704,23 +705,41 @@ impl<'a> Codegen<'a> {
|
||||
|
||||
self.print_orphan_comments_before(first.span().start);
|
||||
|
||||
// Ensure first string literal is not a directive.
|
||||
let mut first_needs_parens = false;
|
||||
if directives.is_empty()
|
||||
&& !self.options.minify
|
||||
&& let Statement::ExpressionStatement(s) = first
|
||||
// If first statement is a string literal, wrap it in parentheses, to prevent it being parsed as a directive.
|
||||
// Only a parenthesized string expression can reach here (a bare one would have been parsed as a directive),
|
||||
// so the parentheses have to be printed back.
|
||||
//
|
||||
// Need to do this regardless of whether or any real directives precede it or not.
|
||||
// Parentheses must be retained for any of:
|
||||
// * `("use strict");`
|
||||
// * `"use asm"; ("use strict");`
|
||||
// * `"use server"; "use asm"; ("use strict");`
|
||||
//
|
||||
// The same hazard exists in minify mode.
|
||||
// Usually strings are printed as template literals, which are not parsed as directives.
|
||||
// But if the string contains a backtick or `${`, it's printed with `"` or `'` quotes,
|
||||
// which *would* be re-parsed as a directive.
|
||||
// So in minify mode, we force printing as a template literal, regardless of the string's content.
|
||||
// In almost all cases, this is shorter than wrapping in parentheses.
|
||||
if let Statement::ExpressionStatement(stmt) = first
|
||||
&& let expr = stmt.expression.without_parentheses()
|
||||
&& let Expression::StringLiteral(string) = expr
|
||||
{
|
||||
let s = s.expression.without_parentheses();
|
||||
if matches!(s, Expression::StringLiteral(_)) {
|
||||
first_needs_parens = true;
|
||||
self.print_ascii_byte(b'(');
|
||||
s.print_expr(self, Precedence::Lowest, ctx);
|
||||
self.print_ascii_byte(b')');
|
||||
self.print_semicolon_after_statement();
|
||||
// Mirror `ExpressionStatement`'s printer, which this path stands in for
|
||||
self.print_comments_at(stmt.span.start);
|
||||
if self.indent > 0 || self.print_next_indent_as_space {
|
||||
self.print_indent();
|
||||
self.add_source_mapping(stmt.span);
|
||||
}
|
||||
}
|
||||
|
||||
if !first_needs_parens {
|
||||
if self.options.minify {
|
||||
self.print_string_literal_as_template(string);
|
||||
} else {
|
||||
self.print_ascii_byte(b'(');
|
||||
self.print_string_literal(string, /* allow_backtick */ true);
|
||||
self.print_ascii_byte(b')');
|
||||
}
|
||||
self.print_semicolon_after_statement();
|
||||
} else {
|
||||
first.print(self, ctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ impl Codegen<'_> {
|
||||
self.print_string_impl(s.value.as_str(), s.lone_surrogates, allow_backtick);
|
||||
}
|
||||
|
||||
/// Print a [`StringLiteral`] as a template literal, whatever its contents.
|
||||
///
|
||||
/// A template literal is never a directive, which a string literal in the first statement position would be.
|
||||
/// See `print_directives_and_statements`.
|
||||
pub(crate) fn print_string_literal_as_template(&mut self, s: &StringLiteral<'_>) {
|
||||
self.add_source_mapping(s.span);
|
||||
Quote::Backtick.print(self);
|
||||
self.print_string_body(s.value.as_str(), s.lone_surrogates, Some(Quote::Backtick), true);
|
||||
}
|
||||
|
||||
pub(super) fn print_string_impl(
|
||||
&mut self,
|
||||
s: &str,
|
||||
@@ -53,6 +63,20 @@ impl Codegen<'_> {
|
||||
Some(quote)
|
||||
};
|
||||
|
||||
self.print_string_body(s, lone_surrogates, quote, allow_backtick);
|
||||
}
|
||||
|
||||
/// Print the contents of a string, and its closing quote.
|
||||
///
|
||||
/// `quote` is `None` where it has yet to be chosen - it is then calculated from the contents,
|
||||
/// and the opening quote printed, when the first character needing an escape is found.
|
||||
fn print_string_body(
|
||||
&mut self,
|
||||
s: &str,
|
||||
lone_surrogates: bool,
|
||||
quote: Option<Quote>,
|
||||
allow_backtick: bool,
|
||||
) {
|
||||
// Loop through bytes, looking for any which need to be escaped.
|
||||
// String is written to buffer in chunks.
|
||||
let bytes = s.as_bytes().iter();
|
||||
|
||||
@@ -956,3 +956,11 @@ fn test_comment_inside_parenthesized_expression_with_pife_arrow() {
|
||||
fn test_comment_inside_double_parenthesized_pife_arrow() {
|
||||
test_idempotency("const x = foo ? bar : ( ( ( a ) => a ) );");
|
||||
}
|
||||
|
||||
// A leading comment on the statement which closes the directive prologue was dropped, because the
|
||||
// paren-protecting path prints that statement itself instead of going through its printer.
|
||||
#[test]
|
||||
fn test_comment_on_paren_protected_prologue_boundary() {
|
||||
test_same("// leading comment\n(\"use strict\");\nfoo();\n");
|
||||
test_same("\"use asm\";\n// leading comment\n(\"use strict\");\nfoo();\n");
|
||||
}
|
||||
@@ -654,6 +654,52 @@ fn directive() {
|
||||
test_options(r#""'\"""#, "\"'\\\"\";\n", double_quote);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directive_prologue_boundary() {
|
||||
// A parenthesized string expression statement has to keep its parentheses while it sits at the
|
||||
// end of the directive prologue, or it re-parses as a directive.
|
||||
|
||||
// No directives before it
|
||||
test_same("(\"use strict\");\nfoo();\n");
|
||||
// After real directives - printed bare, this would switch the program to strict mode
|
||||
test_same("\"use asm\";\n(\"use strict\");\nfoo();\n");
|
||||
// Indented, in a function body
|
||||
test_same("function f() {\n\t(\"x\");\n}\n");
|
||||
test_same("function f() {\n\t\"use strict\";\n\t(\"x\");\n}\n");
|
||||
// A TS module block has a directive prologue too
|
||||
test_same("module Foo {\n\t(\"x\");\n}\n");
|
||||
// A class static block does not, so there is nothing to protect against there
|
||||
test(
|
||||
"class C {\n\tstatic {\n\t\t(\"x\");\n\t}\n}\n",
|
||||
"class C {\n\tstatic {\n\t\t\"x\";\n\t}\n}\n",
|
||||
);
|
||||
|
||||
// Only the statement which closes the prologue needs the parentheses - a string statement
|
||||
// after it cannot be a directive
|
||||
test_same("(\"a\");\n\"b\";\n");
|
||||
test("foo();\n(\"a\");\n", "foo();\n\"a\";\n");
|
||||
test("\"use strict\";\nfoo();\n(\"a\");\n", "\"use strict\";\nfoo();\n\"a\";\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directive_prologue_boundary_minify() {
|
||||
// Minified output has the same hazard. A string usually prints as a template literal, which
|
||||
// cannot be a directive, but only where that is the shortest form - one backtick in the string
|
||||
// is enough to make quotes shorter, and then nothing else would stop it printing as a
|
||||
// directive. So the template literal is printed whatever the contents.
|
||||
test_minify("(\"`\");", "`\\``;");
|
||||
// `${` costs a backtick the same as a backtick does
|
||||
test_minify("(\"${}\");", "`\\${}`;");
|
||||
// Strings which would have chosen a template literal anyway are unaffected
|
||||
test_minify("(\"use strict\");", "`use strict`;");
|
||||
// After real directives
|
||||
test_minify("\"use asm\"; (\"`\");", "\"use asm\";`\\``;");
|
||||
// A newline pays for a backtick, so quotes were never shorter for this one
|
||||
test_minify("(\"\\n`\");", "`\n\\``;");
|
||||
// Only the statement which closes the prologue is at risk
|
||||
test_minify("foo(); (\"`\");", "foo();\"`\";");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getter_setter() {
|
||||
test_minify("({ get [foo]() {} })", "({get[foo](){}});");
|
||||
|
||||
Reference in new issue
Block a user