Skip to content
Closed
8 changes: 8 additions & 0 deletions src/ast/g.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ pub struct Arg {
pub is_typescript_ctor_field: bool,

pub ts_metadata: TypeScript::Metadata,

// Comments that appeared immediately before this argument's binding in the
// source, e.g. `function(/* config.proxies */ proxies) {}`. Preserved so
// that `Function.prototype.toString()` round-trips the DI annotation style
// used by AngularJS / karma's `di` package.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub leading_comments: StoreSlice<Comment>,
}

impl Default for Arg {
Expand All @@ -347,6 +353,7 @@ impl Default for Arg {
default: None,
is_typescript_ctor_field: false,
ts_metadata: TypeScript::Metadata::MNone,
leading_comments: StoreSlice::EMPTY,
}
}
}
Expand All @@ -368,6 +375,7 @@ impl Arg {
},
is_typescript_ctor_field: self.is_typescript_ctor_field,
ts_metadata: self.ts_metadata.clone(),
leading_comments: self.leading_comments,
})
}
}
28 changes: 28 additions & 0 deletions src/js_parser/parse/parse_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(p.s(S::Function { func }, loc))
}

fn take_arg_leading_comments(
&mut self,
base: usize,
) -> bun_ast::StoreSlice<G::Comment> {
let buf = &mut self.lexer.comments_to_preserve_before;
if buf.len() <= base {
return bun_ast::StoreSlice::EMPTY;
}
let slice = self.arena.alloc_slice_fill_iter(buf.drain(base..));
bun_ast::StoreSlice::new_mut(slice)
}

pub(crate) fn parse_fn(
&mut self,
name: Option<js_ast::LocRef>,
Expand All @@ -185,6 +197,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
open_parens_loc: p.lexer.loc(),
..Default::default()
};

// Capture `/* ... */` comments inside the parameter list so they
// survive into `Function.prototype.toString()`. Only entries pushed
// past `comments_base` are ours; anything already in the buffer (legal
// comments from earlier in the statement) is left in place.
Comment thread
robobun marked this conversation as resolved.
Resolved
let old_preserve_comments = p.lexer.preserve_all_comments_before;
let comments_base = p.lexer.comments_to_preserve_before.len();
Comment thread
robobun marked this conversation as resolved.
Resolved
p.lexer.preserve_all_comments_before = true;
p.lexer.expect(T::TOpenParen)?;

// Await and yield are not allowed in function arguments
Expand Down Expand Up @@ -244,6 +264,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
func.flags.insert(Flags::Function::HasRestArg);
}

let leading_comments = p.take_arg_leading_comments(comments_base);

let mut is_typescript_ctor_field = false;
let is_identifier = p.lexer.token == T::TIdentifier;
let mut text = p.lexer.identifier;
Expand Down Expand Up @@ -325,6 +347,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// We need to track this because it affects code generation
is_typescript_ctor_field,
ts_metadata,
leading_comments,
});

if p.lexer.token != T::TComma {
Expand All @@ -346,6 +369,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.lexer.next()?;
rest_arg = false;
}
// Drop any comments captured between the last arg and `)`; they would
// otherwise surface as top-level `SComment` statements in the body.
Comment thread
robobun marked this conversation as resolved.
Resolved
p.lexer.comments_to_preserve_before.truncate(comments_base);
p.lexer.preserve_all_comments_before = old_preserve_comments;

if !args.is_empty() {
func.args = bun_ast::StoreSlice::new_mut(args.into_bump_slice_mut());
}
Expand Down
21 changes: 21 additions & 0 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,27 @@
self.print_space();
}

if !self.options.minify_whitespace {

Check failure on line 2111 in src/js_printer/lib.rs

View check run for this annotation

Claude / Claude Code Review

arg.leading_comments gated on !minify_whitespace drops legal comments under --minify

Legal comments (`/*! ... */`) captured into `arg.leading_comments` are dropped under `--minify` because this whole loop is gated on `!minify_whitespace`. Before this PR, a legal comment like `function foo(/*! @license MIT */ x) {}` flowed through to an `S::Comment` in the body and was printed even when minifying; now `take_arg_leading_comments` drains it out of the buffer and this gate discards it. Either emit comments whose `text[2] == b'!'` here regardless of `minify_whitespace`, or have `take
Comment thread
robobun marked this conversation as resolved.
Resolved
for comment in arg.leading_comments.slice() {
let text = comment.text.slice();
if text.starts_with(b"//") {
// A line comment here would swallow the rest of the
// parameter list on one line; re-emit as a block
// comment. Skip if the body contains "*/" since
// that would terminate the block early.
Comment thread
robobun marked this conversation as resolved.
Resolved
let body = &text[2..];
if !strings::contains(body, b"*/") {
self.print(b"/*");
self.print(body);
self.print(b" */ ");
}
} else {
self.print(text);
self.print(b" ");
}
}
}

if has_rest_arg && i + 1 == args.len() {
self.print(b"...");
}
Expand Down
108 changes: 108 additions & 0 deletions test/regression/issue/13451/13451.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { test, expect } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

// https://github.com/oven-sh/bun/issues/13451
//
// Karma's `di` package (and AngularJS $injector) parse Function.prototype.toString()
// to extract DI tokens from /* comment */-annotated parameter names, e.g.
//
// exports.create = function (/* config */ config, /* config.proxies */ proxies) { ... }
//
// Bun's runtime transpiler was discarding those comments, so `di` saw `proxies`
// instead of `config.proxies` and failed with "No provider for "proxies"!".

const FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
const FN_ARG = /\/\*([^\*]*)\*\//m;

function diParse(src: string): string[] {
const match = src.match(FN_ARGS);
if (!match || !match[1]) return [];
return match[1].split(",").map(arg => {
const m = arg.match(FN_ARG);
return m ? m[1].trim() : arg.trim();
});
}

test("inline /* */ comments in function parameter lists survive the runtime transpiler", async () => {
Comment thread
robobun marked this conversation as resolved.
Resolved
using dir = tempDir("issue-13451", {
"mod.cjs": `
exports.anonExpr = function (/* a */ x, /* b.c */ y, /* d */ z) {
return [x, y, z];
};

function decl(/* one */ p, /* two */ q) {
return [p, q];
}
exports.decl = decl;

exports.obj = {
method(/* inner */ v) {
return v;
},
};

exports.karma = function (/* config */ config, /* config.proxies */ proxies, /* emitter */ emitter) {
return [config, proxies, emitter];
};

exports.lineComment = function (
// token
value
) {
return value;
};
`,
"main.cjs": `
const m = require("./mod.cjs");
console.log(JSON.stringify({
anonExpr: m.anonExpr.toString(),
decl: m.decl.toString(),
method: m.obj.method.toString(),
karma: m.karma.toString(),
lineComment: m.lineComment.toString(),
}));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "main.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
proc.stdout.text(),
proc.stderr.text(),
proc.exited,
]);
expect(stderr).toBe("");

const results = JSON.parse(stdout);

// anonymous function expression
expect(results.anonExpr).toContain("/* a */");
expect(results.anonExpr).toContain("/* b.c */");
expect(results.anonExpr).toContain("/* d */");
expect(diParse(results.anonExpr)).toEqual(["a", "b.c", "d"]);

// named function declaration
expect(results.decl).toContain("/* one */");
expect(results.decl).toContain("/* two */");
expect(diParse(results.decl)).toEqual(["one", "two"]);

// object method shorthand
expect(results.method).toContain("/* inner */");

// karma's exact pattern from middleware/proxy.js
expect(results.karma).toContain("/* config */");
expect(results.karma).toContain("/* config.proxies */");
expect(results.karma).toContain("/* emitter */");
expect(diParse(results.karma)).toEqual(["config", "config.proxies", "emitter"]);

// line comment converted to block (otherwise it would swallow the arg list)
expect(results.lineComment).not.toMatch(/\/\/[^\n]*\)/);
expect(results.lineComment).toContain("token");

expect(exitCode).toBe(0);
});
Loading