Skip to content
107 changes: 99 additions & 8 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,28 @@ struct CollectedRow {

const PADDING: u32 = 1;

/// Parse `s` as a canonical ES array index (0 .. 2^32-2, no leading zeros).
fn parse_array_index(s: &[u8]) -> Option<u32> {
if s.is_empty() || s.len() > 10 {
return None;
}
if s[0] == b'0' {
return if s.len() == 1 { Some(0) } else { None };
}
let mut n: u64 = 0;
for &c in s {
if !c.is_ascii_digit() {
return None;
}
n = n * 10 + u64::from(c - b'0');
}
if n < u64::from(u32::MAX) {
Some(n as u32)
} else {
None
}
}

impl<'a> TablePrinter<'a> {
pub fn init(
global_object: &'a JSGlobalObject,
Expand All @@ -722,7 +744,7 @@ impl<'a> TablePrinter<'a> {
// from a temporary is rejected (E0509).
let mut f = Formatter::new(global_object);
f.single_line = true;
f.max_depth = 5;
f.max_depth = 0;
f.can_throw_stack_overflow = true;
f.stack_check = StackCheck::init();
f
Expand All @@ -732,6 +754,13 @@ impl<'a> TablePrinter<'a> {
})
}

/// Node's `setlike` predicate: `isSet(v) || isSetIterator(v)`. `WeakSet`
/// is not setlike (no `[[SetData]]`), so it falls through to plain-object
/// handling just like any other non-iterable object.
fn is_setlike(&self) -> bool {
self.jstype == jsc::JSType::Set || self.jstype == jsc::JSType::SetIterator
}

/// Format `value` exactly once (bare for strings, quoted otherwise),
/// appending its rendered bytes to the shared `cell_text` scratch, and
/// return the recorded byte range plus its visible width.
Expand Down Expand Up @@ -792,6 +821,16 @@ impl<'a> TablePrinter<'a> {
return Ok(row);
}

// Set entries always go to the "Values" column as a whole, even when
// the entry is an object (Node.js routes Set/SetIterator through a
// dedicated setlike branch that never explodes per-property columns).
if self.is_setlike() {
let cell = self.format_cell::<ENABLE_ANSI_COLORS>(cell_text, row_value)?;
self.values_col_width = Some(self.values_col_width.unwrap_or(1).max(cell.width));
row.values_cell = Some(cell);
return Ok(row);
}

if let Some(obj) = row_value.get_object() {
// object ->
// - if "properties" arg was provided: iterate the already-created
Expand All @@ -812,9 +851,11 @@ impl<'a> TablePrinter<'a> {
let mut cols_iter = jsc::JSPropertyIterator::init(
self.global_object,
obj,
jsc::PropertyIteratorOptions {
jsc::JSPropertyIteratorOptions {
skip_empty_name: false,
include_value: true,
include_symbols: false,
..Default::default()
},
)?;

Expand Down Expand Up @@ -961,14 +1002,21 @@ impl<'a> TablePrinter<'a> {
});
}

// Map/Set route every row through fixed Key/Values columns and never
// read the properties filter (Node returns from its map/setlike
// branches before `properties` is consulted).
let ignores_properties = self.jstype.is_map() || self.is_setlike();

// if the "properties" arg was provided, pre-populate the columns
if !self.properties.is_undefined() {
if !self.properties.is_undefined() && !ignores_properties {
let mut properties_iter = jsc::JSArrayIterator::init(self.properties, global_object)?;
while let Some(value) = properties_iter.next()? {
columns.push(Column {
name: value.to_bun_string(global_object)?,
width: 1,
});
let name = value.to_bun_string(global_object)?;
if columns[1..].iter().any(|col| col.name.eql(&name)) {
name.deref();
continue;
}
columns.push(Column { name, width: 1 });
}
}

Expand Down Expand Up @@ -1035,9 +1083,11 @@ impl<'a> TablePrinter<'a> {
let mut rows_iter = jsc::JSPropertyIterator::init(
global_object,
tabular_obj,
jsc::PropertyIteratorOptions {
jsc::JSPropertyIteratorOptions {
skip_empty_name: false,
include_value: true,
include_symbols: false,
..Default::default()
},
)?;

Expand All @@ -1054,6 +1104,47 @@ impl<'a> TablePrinter<'a> {
}
}

// Node builds the column set by assigning into a null-proto object and
// then reading it back via `Object.keys`, so the final column order is
// the spec's OrdinaryOwnPropertyKeys order: array-index names sorted
// numerically first, then string names in first-seen (insertion) order.
// Reproduce that by stably partitioning the data columns.
if columns.len() > 2 && !self.jstype.is_map() {
let n = columns.len() - 1;
let mut order: Vec<usize> = (0..n).collect();
let keys: Vec<Option<u32>> = columns[1..]
.iter()
.map(|c| {
let utf8 = c.name.to_utf8();
parse_array_index(utf8.slice())
})
.collect();
order.sort_by(|&a, &b| match (keys[a], keys[b]) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => core::cmp::Ordering::Less,
(None, Some(_)) => core::cmp::Ordering::Greater,
(None, None) => a.cmp(&b),
});
if order.iter().enumerate().any(|(i, &o)| i != o) {
let mut new_cols: Vec<Column> = Vec::with_capacity(columns.len());
new_cols.push(core::mem::take(&mut columns[0]));
for &o in &order {
new_cols.push(core::mem::take(&mut columns[1 + o]));
}
*columns = new_cols;
for row in rows.iter_mut() {
let old = core::mem::take(&mut row.cells);
let mut new_cells: Vec<Option<CellRef>> = vec![None; n];
for (new_i, &old_i) in order.iter().enumerate() {
if let Some(cell) = old.get(old_i).copied().flatten() {
new_cells[new_i] = Some(cell);
}
}
row.cells = new_cells;
}
}
}

// append the special "Values" column as the last one, if it is present
if let Some(width) = self.values_col_width {
self.values_col_idx = columns.len();
Expand Down
17 changes: 12 additions & 5 deletions src/jsc/JSPropertyIterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use bun_core as bstr;

/// Runtime flag set passed to [`JSPropertyIterator::init`].
///
/// `Default` is `own_properties_only = true`,
/// `observable = true`, `only_non_index_properties = false`.
/// `Default` is `own_properties_only = true`, `observable = true`,
/// `only_non_index_properties = false`, `include_symbols = true`.
// Runtime flags (not const generics) because the branches gate per-property work, not a
// hot inner loop, and the monomorphization fan-out would be 32 instantiations. Profile
// hot inner loop, and the monomorphization fan-out would be 2^N instantiations. Profile
// if hot.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JSPropertyIteratorOptions {
Expand All @@ -18,10 +18,11 @@ pub struct JSPropertyIteratorOptions {
pub own_properties_only: bool,
pub observable: bool,
pub only_non_index_properties: bool,
pub include_symbols: bool,
}

impl JSPropertyIteratorOptions {
/// Shorthand for the most common call-site shape; the remaining three
/// Shorthand for the most common call-site shape; the remaining
/// options take the [`Default`] values.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub const fn new(skip_empty_name: bool, include_value: bool) -> Self {
Self {
Expand All @@ -30,6 +31,7 @@ impl JSPropertyIteratorOptions {
own_properties_only: true,
observable: true,
only_non_index_properties: false,
include_symbols: true,
}
}
}
Expand All @@ -43,11 +45,12 @@ impl Default for JSPropertyIteratorOptions {
own_properties_only: true,
observable: true,
only_non_index_properties: false,
include_symbols: true,
}
}
}

/// Two-field shorthand of [`JSPropertyIteratorOptions`]; the remaining three options
/// Two-field shorthand of [`JSPropertyIteratorOptions`]; the remaining options
/// take the default values via the `From` conversion.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PropertyIteratorOptions {
Expand Down Expand Up @@ -147,6 +150,7 @@ impl<'a> JSPropertyIterator<'a> {
&mut len,
options.own_properties_only,
options.only_non_index_properties,
options.include_symbols,
)?;
if cfg!(debug_assertions) {
if len > 0 {
Expand Down Expand Up @@ -259,6 +263,7 @@ impl JSPropertyIteratorImpl {
count: &mut usize,
own_properties_only: bool,
only_non_index_properties: bool,
include_symbols: bool,
) -> JsResult<Option<NonNull<JSPropertyIteratorImpl>>> {
// may return null without an exception
let raw = from_js_host_call_generic(global_object, || {
Expand All @@ -268,6 +273,7 @@ impl JSPropertyIteratorImpl {
count,
own_properties_only,
only_non_index_properties,
include_symbols,
)
})?;
Ok(NonNull::new(raw))
Expand Down Expand Up @@ -320,6 +326,7 @@ unsafe extern "C" {
count: &mut usize,
own_properties_only: bool,
only_non_index_properties: bool,
include_symbols: bool,
) -> *mut JSPropertyIteratorImpl;
safe fn Bun__JSPropertyIterator__getNameAndValue(
iter: &mut JSPropertyIteratorImpl,
Expand Down
1 change: 1 addition & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5992,6 +5992,7 @@ impl VirtualMachine {
own_properties_only: true,
observable: false,
only_non_index_properties: true,
include_symbols: true,
},
)?;
let longest_name = iterator.get_longest_property_name().min(10);
Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/JSPropertyIterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class JSPropertyIterator {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(JSPropertyIterator);
};

extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, size_t* count, bool own_properties_only, bool only_non_index_properties)
extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, size_t* count, bool own_properties_only, bool only_non_index_properties, bool include_symbols)
{
auto& vm = JSC::getVM(globalObject);
JSC::JSValue value = JSValue::decode(encodedValue);
Expand All @@ -45,7 +45,7 @@ extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObje
ASSERT(count);

auto scope = DECLARE_THROW_SCOPE(vm);
JSC::PropertyNameArrayBuilder array(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
JSC::PropertyNameArrayBuilder array(vm, include_symbols ? PropertyNameMode::StringsAndSymbols : PropertyNameMode::Strings, PrivateSymbolMode::Exclude);

if (object->hasNonReifiedStaticProperties()) [[unlikely]] {
object->reifyAllStaticProperties(globalObject);
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ pub(crate) fn inspect_table(
enable_colors: false,
add_newline: false,
flush: false,
max_depth: 5,
max_depth: 0,
quote_strings: true,
ordered_properties: false,
single_line: true,
Expand All @@ -698,7 +698,7 @@ pub(crate) fn inspect_table(
value,
properties,
)?;
table_printer.value_formatter.depth = format_options.max_depth;
table_printer.value_formatter.max_depth = format_options.max_depth;
table_printer.value_formatter.ordered_properties = format_options.ordered_properties;
table_printer.value_formatter.single_line = format_options.single_line;

Expand Down
1 change: 1 addition & 0 deletions src/runtime/api/JSTranspiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const PROP_ITER_OPTS: JSPropertyIteratorOptions = JSPropertyIteratorOptions {
own_properties_only: true,
observable: true,
only_non_index_properties: false,
include_symbols: true,
};

impl Config {
Expand Down
1 change: 1 addition & 0 deletions src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,7 @@ impl Expect {
own_properties_only: false,
observable: true,
only_non_index_properties: false,
include_symbols: true,
},
)?;

Expand Down
Loading
Loading