/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/web/console/mod.rs
1 815 строк
55 KB
Divy Srivastava
perf(ext/web): port console/inspect to Rust (cppgc object wrap) (#35087)
14 июн 2026, 15:26
Не верифицирован
14 июн 2026, 15:26
d400a15
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! Rust implementation of the console/inspect machinery that used to live //! in `ext/web/01_console.js`. The JS file is now a thin shim over the ops //! and the cppgc-wrapped `Console` class defined here. #![allow(clippy::all, reason = "ported Rust code with pre-existing lints")] mod css; mod error_fmt; mod inspect; mod preview; mod quote; mod table; mod width; use std::cell::Cell; use std::cell::RefCell; use std::collections::HashMap; use std::time::Instant; use deno_core::GarbageCollected; use deno_core::OpState; use deno_core::op2; use deno_core::v8; use inspect::*; pub use preview::op_preview_entries; const DEFAULT_INDENT: &str = " "; // Default indent string // Upper bound on repeated-indent / pre-allocation counts derived from // caller-controlled values, to avoid usize-saturation panics and multi-GB // allocations on absurd inputs. const MAX_INDENT_REPEAT: usize = 1024; /// Cap a caller-controlled `Array.length` used only as a `Vec::with_capacity` /// hint, so a cheap-to-create sparse array (length up to 2^32-1) can't trigger /// a multi-GB up-front allocation. The Vec still grows to hold real elements. fn capacity_hint(len: u32) -> usize { (len as usize).min(1 << 16) } const STR_ABBREVIATE_SIZE: f64 = 10_000.0; /// Marker symbol set by the JS shim on its `stylizeNoColor` and /// `createStylizeWithColor(...)` functions so the engine can recognize them /// and run the lookup natively. const STYLIZE_MARKER: &str = "Deno.privateConsoleStylize"; // --------------------------------------------------------------------------- // intrinsics #[derive(Default)] struct IntrinsicsSlot(Option<std::rc::Rc<CachedIntrinsics>>); /// Parse-and-cache the intrinsics object (one-time per runtime). The OpState /// borrow is scoped: the engine re-enters JS (which may need OpState, e.g. /// for stack-trace source mapping), so no borrow may be held across the call. fn cached_intrinsics<'s>( scope: &mut v8::PinScope<'s, '_>, state: &std::rc::Rc<RefCell<OpState>>, obj: v8::Local<'s, v8::Object>, ) -> R<std::rc::Rc<CachedIntrinsics>> { { let state = state.borrow(); if let Some(slot) = state.try_borrow::<IntrinsicsSlot>() { if let Some(cached) = &slot.0 { if cached.matches(scope, obj) { return Ok(cached.clone()); } } } } let parsed = std::rc::Rc::new(parse_intrinsics_global(scope, obj)?); state.borrow_mut().put(IntrinsicsSlot(Some(parsed.clone()))); Ok(parsed) } fn parse_intrinsics_global<'s>( scope: &mut v8::PinScope<'s, '_>, obj: v8::Local<'s, v8::Object>, ) -> R<CachedIntrinsics> { // The intrinsics object is normally supplied by the trusted JS shim, but the // ops/`Console` constructor are reachable from user JS with an arbitrary // object, so reject a malformed one with a catchable TypeError rather than // aborting the process on a panic. fn get_fn<'s>( scope: &mut v8::PinScope<'s, '_>, obj: v8::Local<'s, v8::Object>, key: &'static str, ) -> R<v8::Global<v8::Function>> { let v = try_get_str(scope, obj, key) .unwrap_or_else(|| v8::undefined(scope).into()); let f = v8::Local::<v8::Function>::try_from(v).map_err(|_| { type_err(scope, &format!("intrinsics.{key} must be a function")) })?; Ok(v8::Global::new(scope, f)) } fn get_obj<'s>( scope: &mut v8::PinScope<'s, '_>, obj: v8::Local<'s, v8::Object>, key: &'static str, ) -> R<v8::Global<v8::Object>> { let v = try_get_str(scope, obj, key) .unwrap_or_else(|| v8::undefined(scope).into()); let o = v8::Local::<v8::Object>::try_from(v).map_err(|_| { type_err(scope, &format!("intrinsics.{key} must be an object")) })?; Ok(v8::Global::new(scope, o)) } // wellKnown: flat array of [proto, name, constructor, ...] triples. let mut well_known = Vec::new(); let wk = try_get_str(scope, obj, "wellKnown") .and_then(|v| v8::Local::<v8::Array>::try_from(v).ok()); if let Some(wk) = wk { let len = wk.length(); let mut i = 0u32; while i + 3 <= len { let proto = wk .get_index(scope, i) .and_then(|v| v8::Local::<v8::Object>::try_from(v).ok()); let name = wk .get_index(scope, i + 1) .map(|v| v.to_rust_string_lossy(scope)); let ctor = wk .get_index(scope, i + 2) .and_then(|v| v8::Local::<v8::Function>::try_from(v).ok()); if let (Some(proto), Some(name), Some(ctor)) = (proto, name, ctor) { well_known.push(( v8::Global::new(scope, proto), name, v8::Global::new(scope, ctor), )); } i += 3; } } Ok(CachedIntrinsics { key: v8::Global::new(scope, obj), function_to_string: get_fn(scope, obj, "functionToString")?, inspect_fn: get_fn(scope, obj, "inspect")?, stylize_no_color: get_fn(scope, obj, "stylizeNoColor")?, create_stylize_with_color: get_fn(scope, obj, "createStylizeWithColor")?, styles_obj: get_obj(scope, obj, "styles")?, colors_obj: get_obj(scope, obj, "colors")?, object_prototype: get_obj(scope, obj, "objectPrototype")?, error_prototype: get_obj(scope, obj, "errorPrototype")?, well_known, get_url_prototype: get_fn(scope, obj, "getURLPrototype")?, get_cwd: get_fn(scope, obj, "getCwd")?, make_cross_context_stylize: get_fn(scope, obj, "makeCrossContextStylize")?, reg_exp_to_string: get_fn(scope, obj, "regExpToString")?, number_value_of: get_fn(scope, obj, "numberValueOf")?, string_value_of: get_fn(scope, obj, "stringValueOf")?, boolean_value_of: get_fn(scope, obj, "booleanValueOf")?, big_int_value_of: get_fn(scope, obj, "bigIntValueOf")?, symbol_value_of: get_fn(scope, obj, "symbolValueOf")?, }) } // --------------------------------------------------------------------------- // option parsing fn detect_stylize<'s>( scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, value: v8::Local<'s, v8::Value>, ) -> Option<StylizeKind<'s>> { let f = v8::Local::<v8::Function>::try_from(value).ok()?; let marker = symbol_for(scope, STYLIZE_MARKER); let f_obj: v8::Local<v8::Object> = f.into(); let payload = try_get(scope, f_obj, marker.into()); match payload { Some(p) if p.is_string() => { // "noColor" Some(StylizeKind::NoColor) } Some(p) if p.is_array() => { let arr = v8::Local::<v8::Array>::try_from(p).ok()?; let styles = arr .get_index(scope, 0) .and_then(|v| v8::Local::<v8::Object>::try_from(v).ok())?; let colors = arr .get_index(scope, 1) .and_then(|v| v8::Local::<v8::Object>::try_from(v).ok())?; Some(StylizeKind::Theme { styles, colors, js_fn: Some(f), }) } _ => { let _ = intr; Some(StylizeKind::Js(f)) } } } fn default_ctx<'s>() -> Ctx<'s> { Ctx { show_hidden: false, depth: Some(4.0), colors: false, custom_inspect: true, show_proxy: false, max_array_length: 100.0, max_string_length: 10_000.0, break_length: 80.0, escape_sequences: true, compact: Compact::Num(3.0), sorted: Sorted::No, getters: Getters::No, quotes: vec!["\"".to_string(), "'".to_string(), "`".to_string()], iterable_limit: 100.0, trailing_comma: false, indent_level: 0.0, str_abbreviate_size: None, indentation_lvl: 0, current_depth: 0.0, seen: Vec::new(), circular: Vec::new(), circular_set: false, budget: HashMap::new(), stylize: StylizeKind::NoColor, user_options: None, numeric_separator: None, ctx_inspect_fn: None, url_prototype: None, circular_error_message: None, stack_base: None, stylize_js_fn: None, theme_memo: HashMap::new(), } } fn opt_num<'s>( scope: &mut v8::PinScope<'s, '_>, obj: v8::Local<'s, v8::Object>, key: &'static str, ) -> Option<v8::Local<'s, v8::Value>> { try_get_str(scope, obj, key).filter(|v| !v.is_undefined()) } /// `ToNumber(v)` guarded by a `TryCatch`. Option values can be objects with a /// user `valueOf`; an unguarded `number_value` leaves the thrown exception /// pending on the isolate while formatting keeps making V8 calls (DCHECK in /// debug, resurfaces at an arbitrary point in release). Catch and clear it, /// returning NaN, so the rest of option parsing runs on a clean isolate. fn coerce_number<'s>( scope: &mut v8::PinScope<'s, '_>, v: v8::Local<'s, v8::Value>, ) -> f64 { v8::tc_scope!(tc, scope); let n = v.number_value(tc).unwrap_or(f64::NAN); if tc.has_caught() { tc.reset(); return f64::NAN; } n } /// `null` maps to Infinity for maxArrayLength/maxStringLength. fn read_limit<'s>( scope: &mut v8::PinScope<'s, '_>, v: v8::Local<'s, v8::Value>, ) -> f64 { if v.is_null() { f64::INFINITY } else { coerce_number(scope, v) } } /// Apply an options-bag object on top of a ctx (mirrors object spread). /// Iterates the object's own enumerable keys once instead of probing every /// known option (callers often pass small or empty bags). fn apply_options<'s>( scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, options: v8::Local<'s, v8::Object>, ) { let names = { v8::tc_scope!(tc, scope); options.get_property_names( tc, v8::GetPropertyNamesArgs { mode: v8::KeyCollectionMode::OwnOnly, property_filter: v8::PropertyFilter::ONLY_ENUMERABLE | v8::PropertyFilter::SKIP_SYMBOLS, index_filter: v8::IndexFilter::IncludeIndices, key_conversion: v8::KeyConversionMode::ConvertToString, }, ) }; let Some(names) = names else { return; }; for i in 0..names.length() { let Some(key) = names.get_index(scope, i) else { continue; }; let key_str = key.to_rust_string_lossy(scope); let Some(v) = try_get(scope, options, key) else { continue; }; if v.is_undefined() && key_str != "depth" { continue; } match key_str.as_str() { "showHidden" => ctx.show_hidden = v.boolean_value(scope), "depth" => { // Explicit `depth: undefined` (and `null`) means unlimited depth, as // in Node: every depth check is `recurseTimes > depth && depth != null` // and comparisons against undefined/null are false. ctx.depth = if v.is_null() || v.is_undefined() { None } else { Some(coerce_number(scope, v)) }; } "colors" => ctx.colors = v.boolean_value(scope), "customInspect" => ctx.custom_inspect = v.boolean_value(scope), "showProxy" => ctx.show_proxy = v.boolean_value(scope), "maxArrayLength" => ctx.max_array_length = read_limit(scope, v), "maxStringLength" => ctx.max_string_length = read_limit(scope, v), "breakLength" => { ctx.break_length = coerce_number(scope, v); } "escapeSequences" => ctx.escape_sequences = v.boolean_value(scope), "compact" => { ctx.compact = if v.is_number() { Compact::Num(v.number_value(scope).unwrap_or(0.0)) } else { Compact::Bool(v.is_true()) }; } "sorted" => { ctx.sorted = if let Ok(f) = v8::Local::<v8::Function>::try_from(v) { Sorted::Comparator(f) } else if v.boolean_value(scope) { Sorted::Yes } else { Sorted::No }; } "getters" => { ctx.getters = if v.is_string() { match v.to_rust_string_lossy(scope).as_str() { "get" => Getters::Get, "set" => Getters::Set, _ => Getters::No, } } else if v.boolean_value(scope) { Getters::Yes } else { Getters::No }; } "quotes" => { if let Ok(arr) = v8::Local::<v8::Array>::try_from(v) { let mut quotes = Vec::with_capacity(capacity_hint(arr.length())); for i in 0..arr.length() { if let Some(q) = arr.get_index(scope, i) { quotes.push(q.to_rust_string_lossy(scope)); } } ctx.quotes = quotes; } } "iterableLimit" => { ctx.iterable_limit = coerce_number(scope, v); } "trailingComma" => ctx.trailing_comma = v.boolean_value(scope), "indentLevel" => { ctx.indent_level = coerce_number(scope, v); } "strAbbreviateSize" => { ctx.str_abbreviate_size = Some(coerce_number(scope, v)); } "indentationLvl" => { ctx.indentation_lvl = coerce_number(scope, v).max(0.0) as usize; } "numericSeparator" => ctx.numeric_separator = Some(v), "userOptions" => { ctx.user_options = v8::Local::<v8::Object>::try_from(v).ok(); } "inspect" => ctx.ctx_inspect_fn = Some(v), "stylize" => { if let Some(kind) = detect_stylize(scope, intr, v) { ctx.stylize = kind; ctx.stylize_js_fn = v8::Local::<v8::Function>::try_from(v).ok(); } } "seen" => { // Shared seen array (custom-inspect hooks pass the live ctx back). if let Ok(arr) = v8::Local::<v8::Array>::try_from(v) { let mut seen = Vec::with_capacity(capacity_hint(arr.length())); for i in 0..arr.length() { if let Some(item) = arr.get_index(scope, i) { seen.push(item); } } ctx.seen = seen; } } "budget" => { if let Ok(budget_obj) = v8::Local::<v8::Object>::try_from(v) { v8::tc_scope!(tc, scope); if let Some(names) = budget_obj.get_own_property_names(tc, Default::default()) { for i in 0..names.length() { let Some(key) = names.get_index(tc, i) else { continue; }; let key_str = key.to_rust_string_lossy(tc); let Ok(lvl) = key_str.parse::<usize>() else { continue; }; let Some(val) = budget_obj.get(tc, key) else { continue; }; let n = val.number_value(tc).unwrap_or(0.0); // A throwing `valueOf` leaves an exception pending on `tc`; // clear it so the next iteration runs on a clean isolate. if tc.has_caught() { tc.reset(); continue; } ctx.budget.insert(lvl, n.max(0.0) as usize); } } } } _ => {} } } } /// Post-processing shared by `inspect()` and `inspectArgs()`. fn finish_options<'s>( scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, options: Option<v8::Local<'s, v8::Object>>, ) { if let Some(options) = options { // `iterableLimit`/`strAbbreviateSize` are aliases for // `maxArrayLength`/`maxStringLength`; null means unlimited (Infinity), not // ToNumber(null) == 0 (which would truncate everything). if let Some(v) = opt_num(scope, options, "iterableLimit") { ctx.max_array_length = read_limit(scope, v); } if let Some(v) = opt_num(scope, options, "strAbbreviateSize") { ctx.max_string_length = read_limit(scope, v); } } if ctx.colors { ctx.stylize = StylizeKind::Theme { styles: intr.styles_obj(scope), colors: intr.colors_obj(scope), js_fn: None, }; ctx.stylize_js_fn = None; } // maxArrayLength/maxStringLength null already mapped to Infinity. } // --------------------------------------------------------------------------- // inspectArgs fn js_string_char_at(chars: &[u16], i: usize) -> Option<u16> { chars.get(i).copied() } fn utf16_to_string(units: &[u16]) -> String { String::from_utf16_lossy(units) } /// `String(value)` semantics (symbols stringify instead of throwing). fn js_string<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, v8::Value>, ) -> R<String> { if value.is_symbol() { let sym = v8::Local::<v8::Symbol>::try_from(value).unwrap(); let desc = sym.description(scope); return Ok(if desc.is_undefined() { "Symbol()".to_string() } else { format!("Symbol({})", desc.to_rust_string_lossy(scope)) }); } v8::tc_scope!(tc, scope); match value.to_string(tc) { Some(s) => Ok(s.to_rust_string_lossy(tc)), None => { let exc = tc.exception(); Err(grab_err(tc, exc)) } } } /// `NumberParseInt(value)` then template-stringify. fn js_parse_int_display<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, v8::Value>, ) -> R<String> { let s = js_string(scope, value)?; let n = js_parse_int(&s); Ok(js_number_to_string(scope, n)) } fn js_parse_int(s: &str) -> f64 { let t = s.trim_start_matches(|c: char| c.is_whitespace()); let (sign, rest) = match t.strip_prefix('-') { Some(r) => (-1.0, r), None => (1.0, t.strip_prefix('+').unwrap_or(t)), }; let (radix, digits) = if let Some(hex) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) { (16u32, hex) } else { (10u32, rest) }; let mut value: f64 = f64::NAN; let mut acc = 0.0f64; let mut any = false; for c in digits.chars() { match c.to_digit(radix) { Some(d) => { acc = acc * radix as f64 + d as f64; any = true; } None => break, } } if any { value = sign * acc; } value } fn js_parse_float(s: &str) -> f64 { let t = s.trim_start_matches(|c: char| c.is_whitespace()); // Longest prefix that parses as a float literal. let mut end = 0; let bytes = t.as_bytes(); let mut i = 0; if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') { i += 1; } if t[i..].starts_with("Infinity") { return if bytes.first() == Some(&b'-') { f64::NEG_INFINITY } else { f64::INFINITY }; } let mut seen_digit = false; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; seen_digit = true; } if i < bytes.len() && bytes[i] == b'.' { i += 1; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; seen_digit = true; } } if seen_digit { end = i; if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') { let mut j = i + 1; if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') { j += 1; } let mut exp_digit = false; while j < bytes.len() && bytes[j].is_ascii_digit() { j += 1; exp_digit = true; } if exp_digit { end = j; } } } if end == 0 { return f64::NAN; } t[..end].parse::<f64>().unwrap_or(f64::NAN) } fn js_number_to_string<'s>(scope: &mut v8::PinScope<'s, '_>, n: f64) -> String { let num = v8::Number::new(scope, n); num.to_rust_string_lossy(scope) } /// `tryStringify(arg)`: JSON.stringify, mapping circular errors to /// "[Circular]". fn try_stringify<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ctx: &mut Ctx<'s>, value: v8::Local<'s, v8::Value>, ) -> R<String> { let result = { v8::tc_scope!(tc, scope); match v8::json::stringify(tc, value) { Some(s) => Ok(s.to_rust_string_lossy(tc)), None => { let exc = tc.exception(); Err(grab_err(tc, exc)) } } }; match result { Ok(s) => Ok(s), Err(err) => { // Compute the canonical circular error message once. if ctx.circular_error_message.is_none() { let sample = v8::Object::new(scope); let key = v8_str(scope, "a"); sample.set(scope, key.into(), sample.into()); let msg = { v8::tc_scope!(tc, scope); match v8::json::stringify(tc, sample.into()) { Some(_) => String::new(), None => tc .exception() .and_then(|exc| { v8::Local::<v8::Object>::try_from(exc) .ok() .and_then(|o| { let key = v8_str(tc, "message"); o.get(tc, key.into()) }) .map(|m| m.to_rust_string_lossy(tc)) }) .unwrap_or_default(), } }; ctx.circular_error_message = Some(msg.split('\n').next().unwrap_or("").to_string()); } // Only treat TypeErrors with the circular message as circular. let exc = v8::Local::new(scope, &err.0); let is_circular = (|| { let obj = v8::Local::<v8::Object>::try_from(exc).ok()?; let name = try_get_str(scope, obj, "name")?; if name.to_rust_string_lossy(scope) != "TypeError" { return None; } let message = try_get_str(scope, obj, "message")?; let message = message.to_rust_string_lossy(scope); let first_line = message.split('\n').next().unwrap_or(""); if Some(first_line) == ctx.circular_error_message.as_deref() { Some(()) } else { None } })() .is_some(); if is_circular { Ok("[Circular]".to_string()) } else { Err(err) } } } } #[allow(clippy::too_many_arguments, reason = "formatting context")] fn inspect_args_impl<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, args: &[v8::Local<'s, v8::Value>], ) -> R<String> { let no_color = !ctx.colors; let mut a = 0usize; let mut string = String::new(); if !args.is_empty() && args[0].is_string() && args.len() > 1 { let first_str = args[0].to_rust_string_lossy(scope); let first: Vec<u16> = first_str.encode_utf16().collect(); a += 1; let mut appended_chars = 0usize; let mut used_style = false; let mut prev_css: Option<css::Css> = None; let mut i = 0usize; while i + 1 < first.len() { if js_string_char_at(&first, i) == Some(b'%' as u16) { i += 1; let ch = js_string_char_at(&first, i).unwrap_or(0); if a < args.len() { let mut formatted_arg: Option<String> = None; match ch as u8 as char { 's' if ch < 128 => { formatted_arg = Some(js_string(scope, args[a])?); a += 1; } 'd' | 'i' if ch < 128 => { let value = args[a]; a += 1; formatted_arg = Some(if value.is_symbol() { "NaN".to_string() } else { js_parse_int_display(scope, value)? }); } 'f' if ch < 128 => { let value = args[a]; a += 1; formatted_arg = Some(if value.is_symbol() { "NaN".to_string() } else { let s = js_string(scope, value)?; js_number_to_string(scope, js_parse_float(&s)) }); } 'j' if ch < 128 => { let value = args[a]; a += 1; // JSON.stringify yields `undefined` for undefined/function/symbol // top-level values; the old JS left the literal `%j` in the // output rather than substituting the string "undefined". if !(value.is_undefined() || value.is_function() || value.is_symbol()) { formatted_arg = Some(try_stringify(scope, ctx, value)?); } } 'O' | 'o' if ch < 128 => { let value = args[a]; a += 1; formatted_arg = Some(format_value(scope, intr, ctx, value, 0.0, false)?); } 'c' if ch < 128 => { let value = args[a]; a += 1; if !no_color { let css_str = js_string(scope, value)?; let parsed = css::parse_css(&css_str); let ansi = css::css_to_ansi(&parsed, prev_css.as_ref()); if !ansi.is_empty() { used_style = true; prev_css = Some(parsed); } formatted_arg = Some(ansi); } else { formatted_arg = Some(String::new()); } } _ => {} } if let Some(formatted) = formatted_arg { string.push_str(&utf16_to_string(&first[appended_chars..i - 1])); string.push_str(&formatted); appended_chars = i + 1; } } if ch == b'%' as u16 { string.push_str(&utf16_to_string(&first[appended_chars..i - 1])); string.push('%'); appended_chars = i + 1; } } i += 1; } string.push_str(&utf16_to_string(&first[appended_chars..])); if used_style { string.push_str("\u{1b}[0m"); } } while a < args.len() { if a > 0 { string.push(' '); } if args[a].is_string() { string.push_str(&args[a].to_rust_string_lossy(scope)); } else { string.push_str(&format_value(scope, intr, ctx, args[a], 0.0, false)?); } a += 1; } if ctx.indent_level > 0.0 { // Clamp so a huge/Infinite `indentLevel` can't saturate to usize::MAX and // panic in `str::repeat` (the old JS threw a catchable RangeError). let indent = (ctx.indent_level as usize).min(MAX_INDENT_REPEAT); let group_indent = DEFAULT_INDENT.repeat(indent); string = format!( "{group_indent}{}", string.replace('\n', &format!("\n{group_indent}")) ); } Ok(string) } // --------------------------------------------------------------------------- // ops fn rethrow<'s>(scope: &mut v8::PinScope<'s, '_>, err: JsErr) { let exc = v8::Local::new(scope, &err.0); scope.throw_exception(exc); } fn collect_array<'s>( scope: &mut v8::PinScope<'s, '_>, arr: v8::Local<'s, v8::Array>, ) -> Vec<v8::Local<'s, v8::Value>> { let mut out = Vec::with_capacity(capacity_hint(arr.length())); for i in 0..arr.length() { out.push( arr .get_index(scope, i) .unwrap_or_else(|| v8::undefined(scope).into()), ); } out } /// `inspectArgs(args, inspectOptions)`. #[op2(reentrant)] #[string] pub fn op_console_inspect_args<'s>( scope: &mut v8::PinScope<'s, '_>, state: std::rc::Rc<RefCell<OpState>>, intrinsics: v8::Local<'s, v8::Object>, args: v8::Local<'s, v8::Array>, options: v8::Local<'s, v8::Value>, ) -> Option<String> { let cached = match cached_intrinsics(scope, &state, intrinsics) { Ok(c) => c, Err(err) => { rethrow(scope, err); return None; } }; let intr = Intrinsics::new(&cached); let mut ctx = default_ctx(); let options_obj = v8::Local::<v8::Object>::try_from(options).ok(); if let Some(options_obj) = options_obj { apply_options(scope, &intr, &mut ctx, options_obj); } finish_options(scope, &intr, &mut ctx, options_obj); let args = collect_array(scope, args); match inspect_args_impl(scope, &intr, &mut ctx, &args) { Ok(s) => Some(s), Err(err) => { rethrow(scope, err); None } } } /// `inspect(value, inspectOptions)`. #[op2(reentrant)] #[string] pub fn op_console_inspect<'s>( scope: &mut v8::PinScope<'s, '_>, state: std::rc::Rc<RefCell<OpState>>, intrinsics: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, options: v8::Local<'s, v8::Value>, ) -> Option<String> { let cached = match cached_intrinsics(scope, &state, intrinsics) { Ok(c) => c, Err(err) => { rethrow(scope, err); return None; } }; let intr = Intrinsics::new(&cached); let mut ctx = default_ctx(); let options_obj = v8::Local::<v8::Object>::try_from(options).ok(); if let Some(options_obj) = options_obj { apply_options(scope, &intr, &mut ctx, options_obj); } finish_options(scope, &intr, &mut ctx, options_obj); match format_value(scope, &intr, &mut ctx, value, 0.0, false) { Ok(s) => Some(s), Err(err) => { rethrow(scope, err); None } } } /// `formatValue(ctx, value, recurseTimes)` -- node `util.inspect` entry. #[op2(reentrant)] #[string] pub fn op_console_format_value<'s>( scope: &mut v8::PinScope<'s, '_>, state: std::rc::Rc<RefCell<OpState>>, intrinsics: v8::Local<'s, v8::Object>, ctx_obj: v8::Local<'s, v8::Value>, value: v8::Local<'s, v8::Value>, recurse_times: f64, ) -> Option<String> { let cached = match cached_intrinsics(scope, &state, intrinsics) { Ok(c) => c, Err(err) => { rethrow(scope, err); return None; } }; let intr = Intrinsics::new(&cached); let mut ctx = default_ctx(); if let Ok(ctx_o) = v8::Local::<v8::Object>::try_from(ctx_obj) { apply_options(scope, &intr, &mut ctx, ctx_o); } // No finish_options: formatValue honors ctx.stylize as provided. match format_value(scope, &intr, &mut ctx, value, recurse_times, false) { Ok(s) => Some(s), Err(err) => { rethrow(scope, err); None } } } /// `quoteString(string, ctx)`. // // `reentrant`: reading `quotes`/`escapeSequences` off `ctx_obj` runs `Get`, // which can fire user getters / proxy traps that call back into ops. #[op2(reentrant)] #[string] pub fn op_console_quote_string<'s>( scope: &mut v8::PinScope<'s, '_>, #[string] string: String, ctx_obj: v8::Local<'s, v8::Value>, ) -> String { let mut quotes = vec!["\"".to_string(), "'".to_string(), "`".to_string()]; let mut escape_sequences = true; if let Ok(ctx_o) = v8::Local::<v8::Object>::try_from(ctx_obj) { if let Some(v) = opt_num(scope, ctx_o, "quotes") { if let Ok(arr) = v8::Local::<v8::Array>::try_from(v) { quotes.clear(); for i in 0..arr.length() { if let Some(q) = arr.get_index(scope, i) { quotes.push(q.to_rust_string_lossy(scope)); } } } } if let Some(v) = opt_num(scope, ctx_o, "escapeSequences") { escape_sequences = v.boolean_value(scope); } } quote::quote_string(&string, "es, escape_sequences) } #[op2] #[serde] pub fn op_console_parse_css(#[string] css_string: String) -> css::Css { css::parse_css(&css_string) } #[op2] #[serde] pub fn op_console_parse_css_color( #[string] color_string: String, ) -> Option<[u8; 3]> { css::parse_css_color(&color_string) } #[op2] #[string] pub fn op_console_css_to_ansi( #[serde] css_obj: css::Css, #[serde] prev_css: Option<css::Css>, ) -> String { css::css_to_ansi(&css_obj, prev_css.as_ref()) } #[op2(fast)] pub fn op_console_get_string_width( #[string] s: &str, remove_control_chars: bool, ) -> u32 { width::get_string_width(s, remove_control_chars) as u32 } #[op2] #[string] pub fn op_console_strip_vt(#[string] s: &str) -> String { width::strip_vt_control_characters(s) } // --------------------------------------------------------------------------- // Console (cppgc object wrap) pub struct Console { print_func: v8::TracedReference<v8::Function>, intrinsics: v8::TracedReference<v8::Object>, intr_cache: RefCell<Option<std::rc::Rc<CachedIntrinsics>>>, no_color_stdout: v8::TracedReference<v8::Function>, no_color_stderr: v8::TracedReference<v8::Function>, count_map: RefCell<HashMap<String, u64>>, timer_map: RefCell<HashMap<String, Instant>>, indent_level: Cell<f64>, } // SAFETY: all v8 references are traced. unsafe impl GarbageCollected for Console { fn trace(&self, visitor: &mut v8::cppgc::Visitor) { visitor.trace(&self.print_func); visitor.trace(&self.intrinsics); visitor.trace(&self.no_color_stdout); visitor.trace(&self.no_color_stderr); } fn get_name(&self) -> &'static std::ffi::CStr { c"Console" } } impl Console { fn no_color<'s>( &self, scope: &mut v8::PinScope<'s, '_>, stderr: bool, ) -> bool { let f = if stderr { self.no_color_stderr.get(scope) } else { self.no_color_stdout.get(scope) }; let Some(f) = f else { return false; }; let undef: v8::Local<v8::Value> = v8::undefined(scope).into(); match js_call(scope, f, undef, &[]) { Ok(v) => v.boolean_value(scope), Err(_) => false, } } fn print<'s>(&self, scope: &mut v8::PinScope<'s, '_>, msg: &str, level: i32) { let Some(f) = self.print_func.get(scope) else { return; }; let msg = v8_str(scope, msg); let level = v8::Integer::new(scope, level); let undef: v8::Local<v8::Value> = v8::undefined(scope).into(); // Propagate exceptions from the print function (e.g. broken-pipe errors // from `core.print`) out of `console.log`, matching the old JS behavior. if let Err(err) = js_call(scope, f, undef, &[msg.into(), level.into()]) { rethrow(scope, err); } } /// `getConsoleInspectOptions(noColor)`-equivalent ctx. fn console_ctx<'s>( &self, scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, stderr: bool, ) -> Ctx<'s> { let no_color = self.no_color(scope, stderr); let mut ctx = default_ctx(); ctx.colors = !no_color; ctx.indent_level = self.indent_level.get(); if ctx.colors { ctx.stylize = StylizeKind::Theme { styles: intr.styles_obj(scope), colors: intr.colors_obj(scope), js_fn: None, }; } ctx } fn intr<'s>( &self, scope: &mut v8::PinScope<'s, '_>, ) -> R<std::rc::Rc<CachedIntrinsics>> { let obj = self .intrinsics .get(scope) .ok_or_else(|| type_err(scope, "console intrinsics are unavailable"))?; if let Some(cached) = self.intr_cache.borrow().as_ref() { if cached.matches(scope, obj) { return Ok(cached.clone()); } } let parsed = std::rc::Rc::new(parse_intrinsics_global(scope, obj)?); *self.intr_cache.borrow_mut() = Some(parsed.clone()); Ok(parsed) } fn log_with_level<'a>( &self, scope: &mut v8::PinScope<'a, '_>, args: Option<&v8::FunctionCallbackArguments<'a>>, stderr: bool, level: i32, ) { let cached = match self.intr(scope) { Ok(c) => c, Err(err) => { rethrow(scope, err); return; } }; let intr = Intrinsics::new(&cached); let mut ctx = self.console_ctx(scope, &intr, stderr); let args: Vec<v8::Local<v8::Value>> = match args { Some(args) => (0..args.length()).map(|i| args.get(i)).collect(), None => Vec::new(), }; match inspect_args_impl(scope, &intr, &mut ctx, &args) { Ok(s) => self.print(scope, &format!("{s}\n"), level), Err(err) => rethrow(scope, err), } } fn duration_display(duration_ms: f64) -> String { if duration_ms < 1.0 { format!("{duration_ms:.3}") } else if duration_ms < 10.0 { format!("{duration_ms:.2}") } else if duration_ms < 100.0 { format!("{duration_ms:.1}") } else { format!("{}", duration_ms.round() as i64) } } } #[op2] impl Console { #[constructor] #[cppgc] fn constructor<'a>( scope: &mut v8::PinScope<'a, '_>, print_func: v8::Local<'a, v8::Function>, intrinsics: v8::Local<'a, v8::Object>, no_color_stdout: v8::Local<'a, v8::Function>, no_color_stderr: v8::Local<'a, v8::Function>, ) -> Console { Console { print_func: v8::TracedReference::new(scope, print_func), intrinsics: v8::TracedReference::new(scope, intrinsics), intr_cache: RefCell::new(None), no_color_stdout: v8::TracedReference::new(scope, no_color_stdout), no_color_stderr: v8::TracedReference::new(scope, no_color_stderr), count_map: RefCell::new(HashMap::new()), timer_map: RefCell::new(HashMap::new()), indent_level: Cell::new(0.0), } } #[getter] fn indent_level(&self) -> f64 { self.indent_level.get() } #[setter] fn indent_level(&self, value: f64) { self.indent_level.set(value); } #[nofast] #[reentrant] fn log<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { self.log_with_level(scope, args, false, 1); } #[nofast] #[reentrant] fn debug<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { self.log_with_level(scope, args, false, 0); } #[nofast] #[reentrant] fn info<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { self.log_with_level(scope, args, false, 1); } #[nofast] #[reentrant] fn warn<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { self.log_with_level(scope, args, true, 2); } #[nofast] #[reentrant] fn error<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { self.log_with_level(scope, args, true, 3); } #[nofast] #[reentrant] fn dir<'a>( &self, scope: &mut v8::PinScope<'a, '_>, obj: v8::Local<'a, v8::Value>, options: v8::Local<'a, v8::Value>, ) { let cached = match self.intr(scope) { Ok(c) => c, Err(err) => { rethrow(scope, err); return; } }; let intr = Intrinsics::new(&cached); let mut ctx = self.console_ctx(scope, &intr, false); let options_obj = v8::Local::<v8::Object>::try_from(options).ok(); if let Some(options_obj) = options_obj { apply_options(scope, &intr, &mut ctx, options_obj); finish_options(scope, &intr, &mut ctx, Some(options_obj)); } match inspect_args_impl(scope, &intr, &mut ctx, &[obj]) { Ok(s) => self.print(scope, &format!("{s}\n"), 1), Err(err) => rethrow(scope, err), } } #[nofast] #[reentrant] fn assert<'a>( &self, scope: &mut v8::PinScope<'a, '_>, condition: bool, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { if condition { return; } let cached = match self.intr(scope) { Ok(c) => c, Err(err) => { rethrow(scope, err); return; } }; let intr = Intrinsics::new(&cached); let mut ctx = self.console_ctx(scope, &intr, true); let rest: Vec<v8::Local<v8::Value>> = match args { // First vararg is the condition itself; skip it. Some(args) => (1..args.length()).map(|i| args.get(i)).collect(), None => Vec::new(), }; let mut final_args: Vec<v8::Local<v8::Value>> = Vec::new(); if rest.is_empty() { let msg = v8_str(scope, "Assertion failed"); final_args.push(msg.into()); } else if rest[0].is_string() { let first = rest[0].to_rust_string_lossy(scope); let msg = v8_str(scope, &format!("Assertion failed: {first}")); final_args.push(msg.into()); final_args.extend_from_slice(&rest[1..]); } else { let msg = v8_str(scope, "Assertion failed:"); final_args.push(msg.into()); final_args.extend_from_slice(&rest); } match inspect_args_impl(scope, &intr, &mut ctx, &final_args) { Ok(s) => self.print(scope, &format!("{s}\n"), 3), Err(err) => rethrow(scope, err), } } #[nofast] #[reentrant] fn count(&self, scope: &mut v8::PinScope<'_, '_>, #[string] label: String) { let mut map = self.count_map.borrow_mut(); let entry = map.entry(label.clone()).or_insert(0); *entry += 1; let current = *entry; drop(map); self.print(scope, &format!("{label}: {current}\n"), 1); } #[nofast] #[reentrant] fn count_reset( &self, scope: &mut v8::PinScope<'_, '_>, #[string] label: String, ) { let mut map = self.count_map.borrow_mut(); if map.contains_key(&label) { map.insert(label, 0); return; } drop(map); self.print(scope, &format!("Count for '{label}' does not exist\n"), 2); } #[nofast] #[reentrant] fn time(&self, scope: &mut v8::PinScope<'_, '_>, #[string] label: String) { let mut map = self.timer_map.borrow_mut(); if map.contains_key(&label) { drop(map); self.print(scope, &format!("Timer '{label}' already exists\n"), 2); return; } map.insert(label, Instant::now()); } #[nofast] #[reentrant] fn time_log<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[string] label: String, #[varargs] args: Option<&v8::FunctionCallbackArguments<'a>>, ) { let start = self.timer_map.borrow().get(&label).copied(); let Some(start) = start else { self.print(scope, &format!("Timer '{label}' does not exist\n"), 2); return; }; let duration = start.elapsed().as_secs_f64() * 1000.0; let display = Self::duration_display(duration); let cached = match self.intr(scope) { Ok(c) => c, Err(err) => { rethrow(scope, err); return; } }; let intr = Intrinsics::new(&cached); let mut ctx = self.console_ctx(scope, &intr, false); let label_msg = v8_str(scope, &format!("{label}: {display}ms")); let mut final_args: Vec<v8::Local<v8::Value>> = vec![label_msg.into()]; if let Some(args) = args { // First vararg is the label; skip it. for i in 1..args.length() { final_args.push(args.get(i)); } } match inspect_args_impl(scope, &intr, &mut ctx, &final_args) { Ok(s) => self.print(scope, &format!("{s}\n"), 1), Err(err) => rethrow(scope, err), } } #[nofast] #[reentrant] fn time_end( &self, scope: &mut v8::PinScope<'_, '_>, #[string] label: String, ) { let start = self.timer_map.borrow_mut().remove(&label); let Some(start) = start else { self.print(scope, &format!("Timer '{label}' does not exist\n"), 2); return; }; let duration = start.elapsed().as_secs_f64() * 1000.0; let display = Self::duration_display(duration); self.print(scope, &format!("{label}: {display}ms\n"), 1); } #[nofast] #[reentrant] fn clear(&self, scope: &mut v8::PinScope<'_, '_>) { self.indent_level.set(0.0); self.print(scope, "\u{1b}[1;1H", 1); self.print(scope, "\u{1b}[0J", 1); } #[nofast] #[reentrant] fn table<'a>( &self, scope: &mut v8::PinScope<'a, '_>, data: v8::Local<'a, v8::Value>, properties: v8::Local<'a, v8::Value>, ) { let cached = match self.intr(scope) { Ok(c) => c, Err(err) => { rethrow(scope, err); return; } }; let intr = Intrinsics::new(&cached); if let Err(err) = self.table_impl(scope, &intr, data, properties) { rethrow(scope, err); } } } impl Console { fn table_impl<'s>( &self, scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, data: v8::Local<'s, v8::Value>, properties: v8::Local<'s, v8::Value>, ) -> R<()> { // Validate `properties`. let properties: Option<Vec<v8::Local<'s, v8::Value>>> = if properties .is_undefined() { None } else if properties.is_array() { let arr = v8::Local::<v8::Array>::try_from(properties).unwrap(); Some(collect_array(scope, arr)) } else { let type_of = properties.type_of(scope).to_rust_string_lossy(scope); let msg = v8_str( scope, &format!( "The 'properties' argument must be of type Array: received type {type_of}" ), ); let exc = v8::Exception::error(scope, msg); return Err(JsErr(v8::Global::new(scope, exc))); }; // `is_object()` is true for functions in V8 (IsJSReceiver), so functions // must be excluded explicitly to take the log path (old JS keyed on // `typeof data !== "object"`): `console.table(fn)` prints `[Function: fn]`. if data.is_null() || !data.is_object() || data.is_function() { // this.log(data) let mut ctx = self.console_ctx(scope, intr, false); let s = inspect_args_impl(scope, intr, &mut ctx, &[data])?; self.print(scope, &format!("{s}\n"), 1); return Ok(()); } let data_obj = v8::Local::<v8::Object>::try_from(data).unwrap(); // stringifyValue: inspectValueWithQuotes with depth 1, compact, no break. let no_color = self.no_color(scope, false); let mut sctx = default_ctx(); sctx.colors = !no_color; if sctx.colors { sctx.stylize = StylizeKind::Theme { styles: intr.styles_obj(scope), colors: intr.colors_obj(scope), js_fn: None, }; } sctx.depth = Some(1.0); sctx.compact = Compact::Bool(true); sctx.break_length = f64::INFINITY; let is_set_object = data.is_set(); let is_map_object = data.is_map(); let iterator_symbol = v8::Symbol::get_iterator(scope); let has_iterator_fn = { v8::tc_scope!(tc, scope); data_obj .get(tc, iterator_symbol.into()) .map(|v| v.is_function()) .unwrap_or(false) }; let is_iterator_object = !is_set_object && !is_map_object && !data.is_array() && has_iterator_fn; let values_key = "Values"; let index_key = if is_set_object || is_map_object || is_iterator_object { "(iter idx)" } else { "(idx)" }; // resultData enum ResultData<'s> { Object(v8::Local<'s, v8::Object>), MapEntries(Vec<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>)>), List(Vec<v8::Local<'s, v8::Value>>), } let result_data: ResultData<'s> = if is_set_object { let set = v8::Local::<v8::Set>::try_from(data).unwrap(); let arr = set.as_array(scope); ResultData::List(collect_array(scope, arr)) } else if is_map_object { let map = v8::Local::<v8::Map>::try_from(data).unwrap(); let arr = map.as_array(scope); let mut entries = Vec::new(); let mut i = 0u32; while i + 1 < arr.length() { let k = arr .get_index(scope, i) .unwrap_or_else(|| v8::undefined(scope).into()); let v = arr .get_index(scope, i + 1) .unwrap_or_else(|| v8::undefined(scope).into()); entries.push((k, v)); i += 2; } ResultData::MapEntries(entries) } else if is_iterator_object { // ArrayFrom(data) let values = iterate_to_vec(scope, data)?; ResultData::List(values) } else { ResultData::Object(data_obj) }; // keys = ObjectKeys(resultData) let keys: Vec<String> = match &result_data { ResultData::Object(obj) => { v8::tc_scope!(tc, scope); match obj.get_property_names( tc, v8::GetPropertyNamesArgs { mode: v8::KeyCollectionMode::OwnOnly, property_filter: v8::PropertyFilter::ONLY_ENUMERABLE | v8::PropertyFilter::SKIP_SYMBOLS, index_filter: v8::IndexFilter::IncludeIndices, key_conversion: v8::KeyConversionMode::ConvertToString, }, ) { Some(names) => (0..names.length()) .filter_map(|i| names.get_index(tc, i)) .map(|v| v.to_rust_string_lossy(tc)) .collect(), None => Vec::new(), } } ResultData::MapEntries(entries) => { (0..entries.len()).map(|i| i.to_string()).collect() } ResultData::List(values) => { (0..values.len()).map(|i| i.to_string()).collect() } }; let num_rows = keys.len(); let get_row_value = |scope: &mut v8::PinScope<'s, '_>, idx: usize, key: &str| -> R<Option<RowValue<'s>>> { match &result_data { ResultData::Object(obj) => { // Propagate a throwing row getter (main let `data[k]` throw out of // `console.table`) instead of swallowing it to `undefined`. let obj = *obj; let key_v8 = v8_str(scope, key); let v = js_get(scope, obj, key_v8.into())?; Ok(Some(RowValue::Plain(v))) } ResultData::MapEntries(entries) => { Ok(entries.get(idx).map(|(k, v)| RowValue::MapEntry(*k, *v))) } ResultData::List(values) => { Ok(values.get(idx).copied().map(RowValue::Plain)) } } }; enum RowValue<'s> { Plain(v8::Local<'s, v8::Value>), MapEntry(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>), } let properties_strs: Option<Vec<String>> = properties .as_ref() .map(|p| p.iter().map(|v| v.to_rust_string_lossy(scope)).collect()); // objectValues: column name -> rows let mut object_values: Vec<(String, Vec<Option<String>>)> = Vec::new(); if let Some(props) = &properties_strs { for name in props { object_values.push((name.clone(), vec![Some(String::new()); num_rows])); } } let mut index_keys: Vec<Option<String>> = Vec::new(); let mut values: Vec<Option<String>> = Vec::new(); let mut has_primitives = false; let stringify = |scope: &mut v8::PinScope<'s, '_>, sctx: &mut Ctx<'s>, value: v8::Local<'s, v8::Value>| -> R<String> { // inspectValueWithQuotes if value.is_string() { let s = value.to_rust_string_lossy(scope); let abbreviate_size = sctx.str_abbreviate_size.unwrap_or(STR_ABBREVIATE_SIZE); let len16 = utf16_length(&s); let trunc = if (len16 as f64) > abbreviate_size { let max = abbreviate_size.max(0.0) as usize; let mut out = String::new(); let mut count = 0usize; for c in s.chars() { if count >= max { break; } count += c.len_utf16(); if count <= max { out.push(c); } } format!("{out}...") } else { s }; let quoted = quote::quote_string(&trunc, &sctx.quotes, sctx.escape_sequences); sctx.stylize(scope, "ed, "string") } else { format_value(scope, intr, sctx, value, 0.0, false) } }; for (idx, k) in keys.iter().enumerate() { let row = get_row_value(scope, idx, k)?; let (value, map_entry) = match row { Some(RowValue::Plain(v)) => (v, None), Some(RowValue::MapEntry(mk, mv)) => { (v8::undefined(scope).into(), Some((mk, mv))) } None => (v8::undefined(scope).into(), None), }; if let Some((mk, mv)) = map_entry { // Map rows expose Key/Values columns. for (name, cell) in [("Key", mk), (values_key, mv)] { let column = match object_values.iter_mut().find(|(n, _)| n == name) { Some((_, rows)) => rows, None => { object_values .push((name.to_string(), vec![Some(String::new()); num_rows])); &mut object_values.last_mut().unwrap().1 } }; column[idx] = Some(stringify(scope, &mut sctx, cell)?); } values.push(Some(String::new())); index_keys.push(Some(k.clone())); continue; } let primitive = value.is_null() || !(value.is_object() || value.is_function()); if properties_strs.is_none() && primitive { has_primitives = true; values.push(Some(stringify(scope, &mut sctx, value)?)); } else { let value_obj = v8::Local::<v8::Object>::try_from(value).ok(); let row_keys: Vec<String> = match &properties_strs { Some(p) => p.clone(), None => match value_obj { Some(obj) => { v8::tc_scope!(tc, scope); match obj.get_property_names( tc, v8::GetPropertyNamesArgs { mode: v8::KeyCollectionMode::OwnOnly, property_filter: v8::PropertyFilter::ONLY_ENUMERABLE | v8::PropertyFilter::SKIP_SYMBOLS, index_filter: v8::IndexFilter::IncludeIndices, key_conversion: v8::KeyConversionMode::ConvertToString, }, ) { Some(names) => (0..names.length()) .filter_map(|i| names.get_index(tc, i)) .map(|v| v.to_rust_string_lossy(tc)) .collect(), None => Vec::new(), } } None => Vec::new(), }, }; for rk in &row_keys { if !primitive && let Some(obj) = value_obj { let has = { v8::tc_scope!(tc, scope); let key_v8 = v8_str(tc, rk); obj.has(tc, key_v8.into()).unwrap_or(false) }; if has { let column = match object_values.iter_mut().find(|(n, _)| n == rk) { Some((_, rows)) => rows, None => { object_values .push((rk.clone(), vec![Some(String::new()); num_rows])); &mut object_values.last_mut().unwrap().1 } }; let key_v8 = v8_str(scope, rk); let cell = js_get(scope, obj, key_v8.into())?; column[idx] = Some(stringify(scope, &mut sctx, cell)?); } } } values.push(Some(String::new())); } index_keys.push(Some(k.clone())); } // `objectValues` is a JS object in the original implementation, so its // key order follows JS semantics: integer-like keys first in ascending // order, then string keys in insertion order. type ColEntry = (String, Vec<Option<String>>); let mut string_cols: Vec<ColEntry> = Vec::new(); let mut index_cols: Vec<(u32, ColEntry)> = Vec::new(); for entry in object_values { match quote::is_canonical_index(&entry.0) .then(|| entry.0.parse::<u32>().ok()) .flatten() { Some(n) => index_cols.push((n, entry)), None => string_cols.push(entry), } } index_cols.sort_by_key(|(n, _)| *n); let mut object_values: Vec<(String, Vec<Option<String>>)> = index_cols.into_iter().map(|(_, e)| e).collect(); object_values.append(&mut string_cols); let header_keys: Vec<String> = object_values.iter().map(|(n, _)| n.clone()).collect(); let body_values: Vec<Vec<Option<String>>> = object_values.into_iter().map(|(_, rows)| rows).collect(); let mut header_props: Vec<String> = match &properties_strs { Some(p) => p.clone(), None => { let mut hp = header_keys.clone(); if !is_map_object && has_primitives { hp.push(values_key.to_string()); } hp } }; let mut header: Vec<String> = vec![index_key.to_string()]; header.append(&mut header_props); header.retain(|h| !h.is_empty()); let mut body: Vec<Vec<Option<String>>> = vec![index_keys]; body.extend(body_values); body.push(values); let table_str = table::cli_table(&header, &body); // toTable routes through log. let mut ctx = self.console_ctx(scope, intr, false); let table_v8 = v8_str(scope, &table_str); let s = inspect_args_impl(scope, intr, &mut ctx, &[table_v8.into()])?; self.print(scope, &format!("{s}\n"), 1); Ok(()) } } /// `ArrayFrom(iterable)` for the console.table iterator case. fn iterate_to_vec<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, v8::Value>, ) -> R<Vec<v8::Local<'s, v8::Value>>> { let obj = v8::Local::<v8::Object>::try_from(value) .map_err(|_| simple_error(scope, "not iterable"))?; let iterator_symbol = v8::Symbol::get_iterator(scope); let iter_fn = js_get(scope, obj, iterator_symbol.into())?; let iter_fn = v8::Local::<v8::Function>::try_from(iter_fn) .map_err(|_| simple_error(scope, "not iterable"))?; let iterator = js_call(scope, iter_fn, value, &[])?; let iterator_obj = v8::Local::<v8::Object>::try_from(iterator) .map_err(|_| simple_error(scope, "iterator is not an object"))?; let next_fn = js_get_str(scope, iterator_obj, "next")?; let next_fn = v8::Local::<v8::Function>::try_from(next_fn) .map_err(|_| simple_error(scope, "iterator.next is not a function"))?; let mut out = Vec::new(); loop { let step = js_call(scope, next_fn, iterator, &[])?; let step_obj = v8::Local::<v8::Object>::try_from(step) .map_err(|_| simple_error(scope, "iterator result is not an object"))?; let done = js_get_str(scope, step_obj, "done")?; if done.boolean_value(scope) { break; } let item = js_get_str(scope, step_obj, "value")?; out.push(item); if out.len() > 10_000_000 { break; } } Ok(out) } fn simple_error<'s>(scope: &mut v8::PinScope<'s, '_>, msg: &str) -> JsErr { let msg = v8_str(scope, msg); let exc = v8::Exception::type_error(scope, msg); JsErr(v8::Global::new(scope, exc)) }