Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8a0057d
Make coverage, stack frames, call sites and source maps agree with th…
dylan-conway Oct 17, 2026
a16bfa5
List a precise coverage range only where its count differs from its p…
dylan-conway Oct 17, 2026
0d10d83
Keep no pointer to the text JSC compiled in the coverage report, map …
dylan-conway Oct 17, 2026
d237c03
Map require.resolve() of a known path to the call, so that a line wit…
dylan-conway Oct 17, 2026
3de2467
Do not expect a preview from an executable once the stack of the erro…
dylan-conway Oct 17, 2026
3a49b64
List a static block as a function in Profiler.takePreciseCoverage, as…
dylan-conway Oct 17, 2026
c7677ab
Use the WebKit preview with the end of the empty finally, and have a …
dylan-conway Oct 17, 2026
dc49e20
Leave the program out of the function ranges by its parse mode, so th…
dylan-conway Oct 17, 2026
97070d4
Give V8's range ends from the binding, drop the filter for the empty …
dylan-conway Oct 17, 2026
f4575f8
Use the WebKit preview without isFunctionInSource()
dylan-conway Oct 18, 2026
23b9e75
Have the ranges of Profiler.takePreciseCoverage nest as the source do…
dylan-conway Oct 18, 2026
9d232df
Bump the transpiler cache version for the new mappings, and delete th…
dylan-conway Oct 18, 2026
62a01b4
Use the WebKit preview that leaves the end offset of a label to parse…
dylan-conway Oct 18, 2026
6c5273d
Test what is after a function in a block that did not run
dylan-conway Oct 18, 2026
1251787
Use the WebKit preview where what is after a statement goes up to the…
dylan-conway Oct 18, 2026
21dfdb9
Take the mappings of a SourceMap with get_stringish_allow_empty(), so…
dylan-conway Oct 18, 2026
1115ddc
Keep the layout of the header of InternalSourceMap, which the Bun of …
dylan-conway Oct 18, 2026
250cbf2
Use the WebKit preview where the end offset of a block is after its b…
dylan-conway Oct 18, 2026
f6ec02a
Merge remote-tracking branch 'origin/main' into claude/bun-coverage-s…
dylan-conway Oct 20, 2026
39d2ee9
Use the WebKit where the basic blocks of the control flow profiler ma…
dylan-conway Oct 20, 2026
b615157
Test a do while that is the body of an if and a file with nothing to run
dylan-conway Oct 20, 2026
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
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "87ee10da087b63e011bd1ada64d1814a850a4395";
export const WEBKIT_VERSION = "274ebb3cf6008f627197d818e3939d1e2bfc514d";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
7 changes: 7 additions & 0 deletions src/bun_core/string/immutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,13 @@ pub(crate) fn index_of_line_ranges<const LINE_RANGE_COUNT: usize>(
let _ = new_ranges.extend_from_slice(&ranges.as_slice()[1..]); // OOM/capacity: fire-and-forget
ranges = new_ranges;
}
if current_line <= target_line {
let _ = ranges.push(LineRange {
start: prev_end,
// Wrapping cast.
end: text.len() as u32,
}); // OOM/capacity: fire-and-forget
}

ranges
}
Expand Down
4 changes: 2 additions & 2 deletions src/bundler/linker_context/convertStmtsForChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ pub(crate) fn convert_stmts_for_chunk(
core::ptr::read(&raw const s2.func)
},
},
stmt.loc,
stmt2.loc,
);
stmt.data.s_function_mut().unwrap().func.name =
Some(s.default_name);
Expand All @@ -596,7 +596,7 @@ pub(crate) fn convert_stmts_for_chunk(
},
is_export: false,
},
stmt.loc,
stmt2.loc,
);
stmt.data.s_class_mut().unwrap().class.class_name =
Some(s.default_name);
Expand Down
47 changes: 26 additions & 21 deletions src/js/node/inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,27 +307,29 @@ function buildScriptCoverageList(
url = pathToFileURL(url).href;
}

// Outer functions before nested ones, so a stack-based sweep below sees
// enclosing ranges first. Zero-width entries are dropped: V8 never emits
// startOffset === endOffset, and @bcoe/v8-coverage recurses forever on one.
const functions = script.functions
.filter(([start, end]) => start >= 0 && end > start)
.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
const blocks = script.blocks.filter(([start, end]) => start >= 0 && end > start).sort((a, b) => a[0] - b[0]);
// Outer ranges before nested ones, so a stack-based sweep below sees
// enclosing ranges first.
const functions = script.functions.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
const blocks = script.blocks.sort((a, b) => a[0] - b[0] || b[1] - a[1]);

// Assign each basic block to the innermost function range containing it.
const blocksPerFunction: Array<Array<[number, number, number]>> = functions.map(() => []);
const topLevelBlocks: Array<[number, number, number]> = [];
const stack: number[] = [];
let nextFunction = 0;
for (const block of blocks) {
// The script's own, which is not in a function that is the whole script.
if (block[0] === 0 && block[1] === sourceLength) {
topLevelBlocks.push(block);
continue;
}
while (nextFunction < functions.length && functions[nextFunction][0] <= block[0]) {
stack.push(nextFunction);
nextFunction++;
}
// Functions that ended before this block started can no longer contain
// this block or any later one (blocks are sorted by start).
while (stack.length > 0 && functions[stack[stack.length - 1]][1] < block[0]) {
while (stack.length > 0 && functions[stack[stack.length - 1]][1] <= block[0]) {
stack.pop();
}
// The stack is a nesting chain (siblings get popped above), so ends
Expand All @@ -353,19 +355,25 @@ function buildScriptCoverageList(
const scriptExecuted = blocks.some(([, , count]) => count > 0) ? 1 : 0;
const entries: object[] = [];

const toRange = ([startOffset, endOffset, count]: [number, number, number]) => ({
startOffset,
endOffset,
count: callCount ? count : count > 0 ? 1 : 0,
});
// As in V8, a range is listed only where its count is not that of the range it is in.
const toRanges = (outer: [number, number, number], inner: Array<[number, number, number]>) => {
const ranges: Array<{ startOffset: number; endOffset: number; count: number }> = [];
const enclosing: typeof ranges = [];
for (const [startOffset, endOffset, blockCount] of detailed ? [outer, ...inner] : [outer]) {
while (enclosing.length > 0 && enclosing[enclosing.length - 1].endOffset <= startOffset) enclosing.pop();
const count = callCount ? blockCount : blockCount > 0 ? 1 : 0;
if (enclosing.length > 0 && enclosing[enclosing.length - 1].count === count) continue;
const range = { startOffset, endOffset, count };
ranges.push(range);
enclosing.push(range);
}
return ranges;
};

// Whole-script entry. V8 always reports one covering the entire source.
entries.push({
functionName: "",
ranges: [
{ startOffset: 0, endOffset: sourceLength, count: scriptExecuted },
...(detailed ? topLevelBlocks.map(toRange) : []),
],
ranges: toRanges([0, sourceLength, scriptExecuted], topLevelBlocks),
isBlockCoverage: detailed,
});

Expand Down Expand Up @@ -395,10 +403,7 @@ function buildScriptCoverageList(
}
entries.push({
functionName: "",
ranges: [
{ startOffset, endOffset, count: callCount ? count : count > 0 ? 1 : 0 },
...(detailed ? ownBlocks.map(toRange) : []),
],
ranges: toRanges([startOffset, endOffset, count], ownBlocks),
isBlockCoverage: detailed,
});
}
Expand Down
5 changes: 2 additions & 3 deletions src/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,8 @@ function prepareCallSites(_err, callSites) {
const result: CallSiteObject[] = [];
for (let i = 0; i < callSites.length; i++) {
const callSite = callSites[i];
// CallSite#getColumnNumber() is 0-based here but 1-based in V8, and node
// exposes the column under both names.
const columnNumber = (callSite.getColumnNumber() ?? 0) + 1;
// node exposes the column under both names.
const columnNumber = callSite.getColumnNumber() ?? 0;
result.push({
functionName: callSite.getFunctionName() ?? "",
scriptId: `${callSite.getScriptId()}`,
Expand Down
16 changes: 10 additions & 6 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1538,7 +1538,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
);
}

let _ = self.check_dynamic_specifier(arg, state.loc, "import()");
let _ = self.check_dynamic_specifier(arg, arg.loc, "import()");

self.new_expr(
E::Import {
Expand All @@ -1558,7 +1558,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
) -> Expr {
// The argument must be a string
if matches!(arg.data, js_ast::ExprData::EString(_)) {
return self.transpose_require_resolve_known_string(arg);
return self.transpose_require_resolve_known_string(arg, require_resolve_ref.loc);
}

if self.options.warn_about_unbundled_modules {
Expand Down Expand Up @@ -1587,7 +1587,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

#[inline]
pub(crate) fn transpose_require_resolve_known_string(&mut self, arg: Expr) -> Expr {
pub(crate) fn transpose_require_resolve_known_string(
&mut self,
arg: Expr,
loc: bun_ast::Loc,
) -> Expr {
debug_assert!(matches!(arg.data, js_ast::ExprData::EString(_)));

// Ignore calls to import() if the control flow is provably dead here.
Expand Down Expand Up @@ -1620,7 +1624,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
import_record_index,
// .leading_interior_comments = arg.getString().
},
arg.loc,
loc,
)
}

Expand Down Expand Up @@ -1728,7 +1732,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
import_record_index,
unwrapped_id,
},
arg.loc,
state.loc,
);
}

Expand All @@ -1751,7 +1755,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
import_record_index,
..Default::default()
},
arg.loc,
state.loc,
)
}
_ => {
Expand Down
2 changes: 1 addition & 1 deletion src/js_parser/parse/parse_jsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

return Ok(p.new_expr(
E::JSXElement {
tag: end_tag.data.as_expr(),
tag: tag.data.as_expr(),
children: ExprNodeList::move_from_list(children),
properties,
key_prop_index: key_prop_i,
Expand Down
11 changes: 6 additions & 5 deletions src/js_parser/parse/parse_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

p.lexer.expect(T::TFunction)?;
opts.is_export = true;
return p.parse_fn_stmt(loc, opts, Some(async_range));
return p.parse_fn_stmt(async_range.loc, opts, Some(async_range));
}

if Self::IS_TYPESCRIPT_ENABLED {
Expand Down Expand Up @@ -998,7 +998,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
lexical_decl: LexicalDecl::AllowAll,
..Default::default()
};
let stmt = p.parse_fn_stmt(loc, &mut stmt_opts, Some(async_range))?;
let stmt =
p.parse_fn_stmt(async_range.loc, &mut stmt_opts, Some(async_range))?;
if matches!(stmt.data, js_ast::StmtData::STypeScript(_)) {
// This was just a type annotation
return Ok(stmt);
Expand Down Expand Up @@ -1129,7 +1130,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
is_name_optional: true,
..Default::default()
};
let stmt: Stmt = p.parse_class_stmt(loc, &mut stmt_opts)?;
let stmt: Stmt = p.parse_class_stmt(p.lexer.loc(), &mut stmt_opts)?;

// Use the statement name if present, since it's a better name
let default_name: LocRef = 'default_name_getter: {
Expand All @@ -1150,7 +1151,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
js_ast::StmtData::SClass(class) => {
if let Some(_name) = class.class.class_name {
break 'default_name_getter LocRef {
loc: default_loc,
loc: _name.loc,
ref_: _name.ref_,
};
}
Expand Down Expand Up @@ -1832,7 +1833,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if !p.lexer.has_newline_before
&& (p.lexer.token == T::TClass || opts.ts_decorators.is_some())
{
return Ok(Some(p.parse_class_stmt(loc, opts)?));
return Ok(Some(p.parse_class_stmt(p.lexer.loc(), opts)?));
}
if opts.ts_decorators.is_some() {
let r = js_lexer::range_of_identifier(p.source, loc);
Expand Down
5 changes: 3 additions & 2 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1856,7 +1856,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Data::EImport(nt) if core::ptr::eq(&raw const *e_, &raw const *nt)
),
import_options: e_.options,
loc: e_.expr.loc,
loc: e.loc,
import_loader: e_.import_record_loader(),
..Default::default()
};
Expand Down Expand Up @@ -2171,6 +2171,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let state = TransposeState {
is_require_immediately_assigned_to_decl: in_.is_immediately_assigned_to_decl
&& matches!(first.data, Data::EString(..)),
loc: expr.loc,
..Default::default()
};
match &first.data {
Expand Down Expand Up @@ -2232,7 +2233,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Data::EString(..) => {
// require.resolve(FOO) => require.resolve(FOO)
// (this will register dependencies)
*e = p.transpose_require_resolve_known_string(first);
*e = p.transpose_require_resolve_known_string(first, expr.loc);
return;
}
Data::EIf(..) => {
Expand Down
21 changes: 13 additions & 8 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2978,8 +2978,6 @@ pub(crate) mod __gated_printer {
}

// External import()
self.add_source_mapping(record.range.loc);

self.print_space_before_identifier();

let preload = record.flags.contains(ImportRecordFlags::IMPORTS_CHUNK)
Expand Down Expand Up @@ -3576,6 +3574,7 @@ pub(crate) mod __gated_printer {
}
}
ExprData::ERequireString(e) => {
self.add_source_mapping(expr.loc);
Comment thread
claude[bot] marked this conversation as resolved.
self.print_require_or_import_expr(
e.import_record_index,
e.unwrapped_id.is_some(),
Expand All @@ -3592,6 +3591,7 @@ pub(crate) mod __gated_printer {
}

self.print_space_before_identifier();
self.add_source_mapping(expr.loc);

if let Some(require_ref) = self.options.require_ref {
self.print_symbol(require_ref);
Expand Down Expand Up @@ -3644,6 +3644,7 @@ pub(crate) mod __gated_printer {
self.print(b")");
}
} else {
self.add_source_mapping(expr.loc);
self.print_require_or_import_expr(
e.import_record_index,
false,
Expand Down Expand Up @@ -5392,17 +5393,18 @@ pub(crate) mod __gated_printer {
StmtData::SFunction(s) => {
self.print_indent();
self.print_space_before_identifier();
self.add_source_mapping(stmt.loc);
let name = s.func.name.as_ref().unwrap_or_else(|| {
Output::panic(format_args!(
"Internal error: expected func to have a name ref"
))
});
let name_ref = name.ref_;

// `stmt.loc` is what follows `export`.
if s.func.flags.contains(G::FnFlags::IsExport) {
self.print(b"export ");
}
self.add_source_mapping(stmt.loc);
Comment thread
dylan-conway marked this conversation as resolved.
if s.func.flags.contains(G::FnFlags::IsAsync) {
self.print(b"async ");
}
Expand Down Expand Up @@ -5436,12 +5438,12 @@ pub(crate) mod __gated_printer {

self.print_indent();
self.print_space_before_identifier();
self.add_source_mapping(stmt.loc);
let name_ref = s.class.class_name.as_ref().unwrap().ref_;
if s.is_export {
self.print(b"export ");
}

self.add_source_mapping(stmt.loc);
self.print(b"class ");
self.add_source_mapping(s.class.class_name.as_ref().unwrap().loc);
let name_str = self.name_for_symbol(name_ref);
Expand Down Expand Up @@ -5470,7 +5472,9 @@ pub(crate) mod __gated_printer {
StmtData::SExportDefault(s) => {
self.print_indent();
self.print_space_before_identifier();
self.add_source_mapping(stmt.loc);
if let js_ast::StmtOrExpr::Expr(_) = s.value {
self.add_source_mapping(stmt.loc);
}
self.print(b"export default ");

match s.value {
Expand All @@ -5493,6 +5497,7 @@ pub(crate) mod __gated_printer {
return Ok(());
}
js_ast::StmtOrExpr::Stmt(s2) => {
self.add_source_mapping(s2.loc);
match &s2.data {
StmtData::SFunction(func) => {
self.print_space_before_identifier();
Expand Down Expand Up @@ -6663,8 +6668,7 @@ pub(crate) mod __gated_printer {
self.print_semicolon_if_needed();
self.print_indent();
self.print_space_before_identifier();
self.add_source_mapping(loc);
self.print_decl_stmt(is_export, kind, decls);
self.print_decl_stmt(loc, is_export, kind, decls);
}

/// The initializer of such a declarator, as a statement. Its value is
Expand Down Expand Up @@ -6786,13 +6790,15 @@ pub(crate) mod __gated_printer {

pub(crate) fn print_decl_stmt(
&mut self,
loc: bun_ast::Loc,
is_export: bool,
kind: S::Kind,
decls: &[G::Decl],
) {
if is_export {
self.print(b"export ");
}
self.add_source_mapping(loc);
let tlm: TopLevelAndIsExport = if Self::MAY_HAVE_MODULE_INFO {
TopLevelAndIsExport { is_export }
} else {
Expand Down Expand Up @@ -7645,7 +7651,6 @@ pub(crate) fn get_source_map_builder<'a, const IS_BUN_PLATFORM: bool>(
IS_BUN_PLATFORM && generate_source_map == GenerateSourceMap::Lazy,
),
cover_lines_without_mappings: true,
approximate_input_line_count: tree.approximate_newline_count,
prepend_count: IS_BUN_PLATFORM && generate_source_map == GenerateSourceMap::Lazy,
line_offset_tables: match opts.line_offset_tables.take() {
Some(table) => LineOffsetTables::Borrowed(table),
Expand Down
Loading
Loading