Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
288 changes: 288 additions & 0 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,7 @@
NativeCode,

JSX,
DOMNode,
Event,

GetterSetter,
Expand Down Expand Up @@ -2010,6 +2011,7 @@
| Tag::Error
| Tag::Class
| Tag::Event
| Tag::DOMNode
)
}
}
Expand Down Expand Up @@ -2044,6 +2046,7 @@
ToJSON,
NativeCode,
JSX,
DOMNode,
Event,
GetterSetter,
CustomGetterSetter,
Expand Down Expand Up @@ -2090,6 +2093,7 @@
TagPayload::ToJSON => Tag::ToJSON,
TagPayload::NativeCode => Tag::NativeCode,
TagPayload::JSX => Tag::JSX,
TagPayload::DOMNode => Tag::DOMNode,
TagPayload::Event => Tag::Event,
TagPayload::GetterSetter => Tag::GetterSetter,
TagPayload::CustomGetterSetter => Tag::CustomGetterSetter,
Expand Down Expand Up @@ -2135,6 +2139,7 @@
Tag::ToJSON => TagPayload::ToJSON,
Tag::NativeCode => TagPayload::NativeCode,
Tag::JSX => TagPayload::JSX,
Tag::DOMNode => TagPayload::DOMNode,
Tag::Event => TagPayload::Event,
Tag::GetterSetter => TagPayload::GetterSetter,
Tag::CustomGetterSetter => TagPayload::CustomGetterSetter,
Expand Down Expand Up @@ -2330,6 +2335,16 @@
}
}

// Is this a DOM node (jsdom / happy-dom)?
if matches!(js_type, jsc::JSType::Object | jsc::JSType::FinalObject)
&& is_dom_node(global_this, value)?
{
return Ok(TagResult {
tag: TagPayload::DOMNode,
cell: js_type,
});
}

use jsc::JSType as T;
let tag = match js_type {
T::ErrorInstance => TagPayload::Error,
Expand Down Expand Up @@ -3304,6 +3319,73 @@
Ok(None)
}

pub const DOM_ELEMENT_NODE: i32 = 1;
pub const DOM_TEXT_NODE: i32 = 3;
pub const DOM_COMMENT_NODE: i32 = 8;
pub const DOM_FRAGMENT_NODE: i32 = 11;

/// Constructor name → expected `nodeType` (pretty-format `DOMElement` plugin rule).
pub fn dom_node_type_for_class_name(name: &[u8]) -> Option<i32> {
use bun_core::strings;
// /^((HTML|SVG)\w*)?Element$/
if strings::has_suffix_comptime(name, b"Element")
&& (name.len() == b"Element".len()
|| strings::has_prefix_comptime(name, b"HTML")
|| strings::has_prefix_comptime(name, b"SVG"))
{
return Some(DOM_ELEMENT_NODE);
}
match name {
b"Text" => Some(DOM_TEXT_NODE),
b"Comment" => Some(DOM_COMMENT_NODE),
b"DocumentFragment" => Some(DOM_FRAGMENT_NODE),
_ => None,
}
}

/// True when `value` duck-types as a jsdom/happy-dom node. `#[inline(never)]` keeps `Tag::get` frames small.
#[inline(never)]
pub fn is_dom_node(global_this: &JSGlobalObject, value: JSValue) -> JsResult<bool> {
// DOM nodes inherit through `Node`→`EventTarget`; a null grand-proto means plain `{}`.
let proto = value.get_prototype(global_this);
if proto.is_empty_or_undefined_or_null()
|| !proto.is_cell()
|| proto.js_type() == jsc::JSType::ProxyObject
{
return Ok(false);
}
let grand = proto.get_prototype(global_this);
if grand.is_empty_or_undefined_or_null() {
return Ok(false);
}
Comment thread
claude[bot] marked this conversation as resolved.

let mut name_str = ZigString::init(b"");
value.get_class_name(global_this, &mut name_str)?;
let name_slice = name_str.to_slice();
let expected = match dom_node_type_for_class_name(name_slice.slice()) {
Some(e) => e,
// Custom elements: the prototype chain still goes through `HTMLElement`.
None => {
let mut proto_name = ZigString::init(b"");
grand.get_class_name(global_this, &mut proto_name)?;
let proto_slice = proto_name.to_slice();
match dom_node_type_for_class_name(proto_slice.slice()) {
Some(DOM_ELEMENT_NODE) => DOM_ELEMENT_NODE,
_ => return Ok(false),
}
}
Comment thread
claude[bot] marked this conversation as resolved.
};
let confirmed = match value.get(global_this, "nodeType") {
Ok(Some(n)) if n.is_int32() => n.to_int32() == expected,
Ok(_) => false,
Err(_) => {
global_this.clear_exception_except_termination();
false
}
};
Ok(confirmed)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// `JSGlobalObject` is an opaque `UnsafeCell`-backed ZST handle; remaining
// params are by-value `JSValue`/scalars → `safe fn`.
unsafe extern "C" {
Expand Down Expand Up @@ -3470,6 +3552,7 @@
&mut remove_before_recurse,
),
Tag::JSX => self.print_jsx::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::DOMNode => self.print_dom_node::<ENABLE_ANSI_COLORS>(writer_, value),
Tag::Object => self.print_object::<ENABLE_ANSI_COLORS>(writer_, value, js_type),
Tag::TypedArray => {
self.print_typed_array::<ENABLE_ANSI_COLORS>(writer_, value, js_type)
Expand Down Expand Up @@ -5449,6 +5532,211 @@
Ok(())
}

/// Prints a jsdom/happy-dom node as markup (`<button id="x">…</button>`).
#[inline(never)]
fn print_dom_node<const C: bool>(
&mut self,
writer_: &mut dyn bun_io::Write,
value: JSValue,
) -> JsResult<()> {
macro_rules! pf {
($s:literal) => {
pfmt!($s, C)
};
}
macro_rules! get_swallow {
($v:expr, $name:literal) => {
match $v.get(self.global_this, $name) {
Ok(v) => v,
Err(_) => {
self.global_this.clear_exception_except_termination();
None
}
}
};
}

let node_type = get_swallow!(value, "nodeType")
.filter(|v| v.is_int32())
.map(|v| v.to_int32())
.unwrap_or(0);

if matches!(node_type, DOM_TEXT_NODE | DOM_COMMENT_NODE) {
let data = get_swallow!(value, "data");
let text = match data {
Some(v) if v.is_string() => {
bun_core::OwnedString::new(v.to_bun_string(self.global_this)?)
}
_ => bun_core::OwnedString::new(bun_core::String::empty()),
};
if node_type == DOM_COMMENT_NODE {
let _ = write!(writer_, "{}<!--{}-->{}", pf!("<r><d>"), text, pf!("<r>"));
} else {
let _ = write!(writer_, "{}", text);
}
return Ok(());
}
Comment thread
robobun marked this conversation as resolved.

let (tag_utf8, is_fragment) = if node_type == DOM_FRAGMENT_NODE {
(
bun_core::ZigStringSlice::from_utf8_never_free(b"DocumentFragment"),
true,
)
} else {
let tag_name = get_swallow!(value, "tagName");
match tag_name {
Some(v) if v.is_string() => {
let s = v.get_zig_string(self.global_this)?;
let slice = s.to_slice();
let mut owned = slice.slice().to_vec();
owned.make_ascii_lowercase();
(bun_core::ZigStringSlice::init_owned(owned), false)
}
_ => (
bun_core::ZigStringSlice::from_utf8_never_free(b"unknown"),
false,
),
}
};
let tag_bytes = tag_utf8.slice();

if self.depth >= self.max_depth {
let _ = write!(
writer_,
"{}<{}{} \u{2026} {}/>{}",
pf!("<r><green>"),
bstr::BStr::new(tag_bytes),
pf!("<r>"),
pf!("<green>"),
pf!("<r>"),
);
return Ok(());
}

let _ = writer_.write_all(pf!("<r><green>").as_bytes());
let _ = writer_.write_all(b"<");
let _ = writer_.write_all(tag_bytes);
let _ = writer_.write_all(pf!("<r>").as_bytes());

let mut attrs_multiline = false;
if !is_fragment {
if let Some(attrs) = get_swallow!(value, "attributes") {
if attrs.is_cell() && attrs.is_object() {
if let Some(len_v) = get_swallow!(attrs, "length") {
if len_v.is_int32() {
let n = len_v.to_int32().max(0) as u32;
let mut pairs: Vec<(Vec<u8>, Vec<u8>)> =
Vec::with_capacity(n.min(64) as usize);
for i in 0..n {

Check warning on line 5630 in src/jsc/ConsoleObject.rs

View check run for this annotation

Claude / Claude Code Review

attributes.length / childNodes.length used as unbounded loop counts

Nit: `n = len_v.to_int32().max(0) as u32` is used unclamped as the `for i in 0..n` bound for both `attributes` and `childNodes` (here and in the `pretty_format.rs` twin) — a duck-typed lookalike with `attributes = { length: 0x7fffffff }` drives ~2^31 `get_index` calls, and the `childNodes` loop writes `\n`+indent *before* the `is_cell` guard so a lying `childNodes.length` also blows up the output buffer. `Vec::with_capacity(n.min(64))` already clamps allocation; consider clamping the iteration b
Comment thread
robobun marked this conversation as resolved.
let Ok(attr) = attrs.get_index(self.global_this, i) else {
self.global_this.clear_exception_except_termination();
continue;
};
if !attr.is_cell() || !attr.is_object() {
continue;
}
let Some(name) = get_swallow!(attr, "name") else {
continue;
};
if !name.is_string() {
continue;
}
let name_s = name.get_zig_string(self.global_this)?;
let name_owned = name_s.to_slice().slice().to_vec();
let val = match get_swallow!(attr, "value") {
Some(v) if v.is_string() => v
.get_zig_string(self.global_this)?
.to_slice()
.slice()
.to_vec(),
_ => Vec::new(),
};
pairs.push((name_owned, val));
}
pairs.sort_by(|a, b| a.0.cmp(&b.0));
Comment thread
claude[bot] marked this conversation as resolved.
attrs_multiline = !self.single_line && pairs.len() > 1;
for (name, val) in &pairs {
if attrs_multiline {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent + 1, writer_);
} else {
let _ = writer_.write_all(b" ");
}
let _ = write!(
writer_,
"{}{}{}={}{}{}",
pf!("<blue>"),
bstr::BStr::new(name),
pf!("<r><d>"),
pf!("<r><green>"),
bun_core::fmt::quote(val),
pf!("<r>"),
);

Check warning on line 5674 in src/jsc/ConsoleObject.rs

View check run for this annotation

Claude / Claude Code Review

bun_core::fmt::quote does not escape lone backslashes in DOM attribute values

🟡 Nit: `bun_core::fmt::quote(val)` only escapes `"` — its fast-path predicate `contains_newline_or_non_ascii_or_quote` does not test for `\` (0x5C), so `el.setAttribute("data-path", "C:\\Users")` still emits `data-path="C:\Users"` where pretty-format emits `data-path="C:\\Users"`. The resolved comment on this line asked for both `\` and `"`; commit 408bc378 covered only `"`. Same gap in the `pretty_format.rs` twin. Routing the attribute `JSValue` through the quoted-string printer with `quote_str
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
}
if attrs_multiline {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
}
}
}
}
}

let children =
get_swallow!(value, "childNodes").filter(|v| v.is_cell() && v.is_object());
let child_len = children
.and_then(|c| get_swallow!(c, "length"))
.filter(|v| v.is_int32())
.map(|v| v.to_int32().max(0) as u32)
.unwrap_or(0);

if child_len == 0 {
if attrs_multiline {
let _ = write!(writer_, "{}/>{}", pf!("<green>"), pf!("<r>"));
} else {
let _ = write!(writer_, "{} />{}", pf!("<green>"), pf!("<r>"));
}
return Ok(());
}

let _ = write!(writer_, "{}>{}", pf!("<green>"), pf!("<r>"));
{
self.indent += 1;
self.depth += 1;
let _ind = defer_decrement!(self.indent);
let _dep = defer_decrement!(self.depth);
let children = children.expect("child_len > 0 implies Some");
for i in 0..child_len {
if !self.single_line {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
let Ok(child) = children.get_index(self.global_this, i) else {
self.global_this.clear_exception_except_termination();
continue;
};
if !child.is_cell() {
continue;
}
let tag = Tag::get_advanced(child, self.global_this, self.tag_opts())?;
self.format::<C>(tag, writer_, child, self.global_this)?;
}
}
if !self.single_line {
let _ = writer_.write_all(b"\n");
let _ = write_indent_n(self.indent, writer_);
}
let _ = write!(
writer_,
"{}</{}>{}",
pf!("<green>"),
bstr::BStr::new(tag_bytes),
pf!("<r>"),
);
Ok(())
}

#[inline(never)]
fn print_object<const C: bool>(
&mut self,
Expand Down
Loading
Loading