import { JSStringValue, ObjectValue, Value, wellKnownSymbols, BooleanValue, NumberValue, BigIntValue, SymbolValue, UndefinedValue, } from '../value.mts'; import { Completion, X } from '../completion.mts'; import { isRegExpObject } from '../intrinsics/RegExp.mts'; import type { DateObject } from '../intrinsics/Date.mts'; import type { BooleanObject } from '../intrinsics/Boolean.mts'; import type { NumberObject } from '../intrinsics/Number.mts'; import type { BigIntObject } from '../intrinsics/BigInt.mts'; import type { StringObject } from '../intrinsics/String.mts'; import type { SymbolObject } from '../intrinsics/Symbol.mts'; import { isTypedArrayObject } from '../intrinsics/TypedArray.mts'; import { isShadowRealmObject } from '../intrinsics/ShadowRealm.mts'; import { surroundingAgent } from './engine.mts'; import { Call, IsArray, Get, LengthOfArrayLike, EscapeRegExpPattern, R, type BuiltinFunctionObject, Realm, type Descriptor, type ValueCompletion, type PromiseObject, CyclicModuleRecord, } from '#self'; const bareKeyRe = /^[a-zA-Z_][a-zA-Z_0-9]*$/; function getObjectTag(value: ObjectValue, wrap = false): string { let s = ''; try { s = (X(Get(value, wellKnownSymbols.toStringTag)) as JSStringValue).stringValue(); } catch { } try { const c = X(Get(value, Value('constructor'))); s = (X(Get(c as ObjectValue, Value('name'))) as JSStringValue).stringValue(); } catch { } if (s) { if (wrap) { return `[${s}] `; } return s; } return ''; } const compactObject = (realm: Realm, value: ObjectValue) => { try { const toString = X(Get(value, Value('toString'))) as BuiltinFunctionObject; const objectToString = realm.Intrinsics['%Object.prototype.toString%']; if (toString.nativeFunction === objectToString.nativeFunction) { return (X(Call(toString, value)) as JSStringValue).stringValue(); } else { const tag = getObjectTag(value, false) || 'Unknown'; const ctor = X(Get(value, Value('constructor'))); if (ctor instanceof ObjectValue) { const ctorName = (X(Get(ctor, Value('name'))) as JSStringValue).stringValue(); if (ctorName !== '') { return `#<${ctorName}>`; } return `[object ${tag}]`; } return `[object ${tag}]`; } } catch (e) { return '[object Unknown]'; } }; interface InspectContext { realm: Realm; indent: number; inspected: Value[]; compact: boolean; } // eslint-disable-next-line @typescript-eslint/no-explicit-any type Inspector = (value: any, context: InspectContext, inner: (v: Value) => string) => string; const INSPECTORS = { Null: () => 'null', Undefined: () => 'undefined', Boolean: (v: BooleanValue) => v.booleanValue().toString(), Number: (v: NumberValue) => { const n = R(v); if (n === 0 && Object.is(n, -0)) { return '-0'; } return n.toString(); }, BigInt: (v: BigIntValue) => `${R(v)}n`, String: (v: JSStringValue) => { const s = JSON.stringify(v.stringValue()).slice(1, -1); return `'${s}'`; }, Symbol: (v: SymbolValue) => `Symbol(${v.Description instanceof UndefinedValue ? '' : v.Description.stringValue()})`, Object: (v: ObjectValue, ctx, i) => { if (ctx.inspected.includes(v)) { return '[Circular]'; } if ('PromiseState' in v) { ctx.indent += 1; const result = v.PromiseState === 'pending' ? 'undefined' : i((v as PromiseObject).PromiseResult!); ctx.indent -= 1; return `Promise { [[PromiseState]]: ${v.PromiseState} [[PromiseResult]]: ${result} }`; } if ( 'Module' in v && v.Module instanceof CyclicModuleRecord && v.Module.Status === 'linked' && v.Module.DeferredNamespace === v ) { // Do not read the namespace to avoid triggering the module evaluation. return 'Deferred Module { ... }'; } if ('Call' in v) { const name = v.properties.get('name'); if (name && (name.Value! as JSStringValue).stringValue() !== '') { return `[Function: ${(name.Value as JSStringValue).stringValue()}]`; } return '[Function]'; } if ('ErrorData' in v) { let e = X(Get(v, Value('stack'))); if (!(e as JSStringValue).stringValue) { const toString = X(Get(v, Value('toString'))); e = X(Call(toString, v)); } return (e as JSStringValue).stringValue(); } if (isRegExpObject(v)) { const P = EscapeRegExpPattern(v.OriginalSource, v.OriginalFlags).stringValue(); const F = v.OriginalFlags.stringValue(); return `/${P}/${F}`; } if ('DateValue' in v) { const d = new Date((v as DateObject).DateValue); if (Number.isNaN(d.getTime())) { return '[Date Invalid]'; } return `[Date ${d.toISOString()}]`; } if ('BooleanData' in v) { return `[Boolean ${i((v as BooleanObject).BooleanData)}]`; } if ('NumberData' in v) { return `[Number ${i((v as NumberObject).NumberData)}]`; } if ('BigIntData' in v) { return `[BigInt ${i((v as BigIntObject).BigIntData)}]`; } if ('StringData' in v) { return `[String ${i((v as StringObject).StringData)}]`; } if ('SymbolData' in v) { return `[Symbol ${i((v as SymbolObject).SymbolData)}]`; } if (isShadowRealmObject(v)) { return '[ShadowRealm]'; } ctx.indent += 1; ctx.inspected.push(v); try { const isArray = IsArray(v) === Value.true; const isTypedArray = isTypedArrayObject(v); if (isArray || isTypedArray) { const length = X(LengthOfArrayLike(v)); let holes = 0; const flushHoles = () => { if (holes > 0) { out.push(`<${holes} empty items>`); holes = 0; } }; const out = []; for (let j = 0; j < length; j += 1) { const elem = X(v.GetOwnProperty(Value(j.toString()))); if (elem instanceof UndefinedValue) { holes += 1; } else { flushHoles(); if (elem.Value) { out.push(i(elem.Value)); } else { out.push(''); } } } flushHoles(); return `${isTypedArray ? `${v.TypedArrayName.stringValue()} ` : ''}[${out.join(', ')}]`; } const keys = X(v.OwnPropertyKeys()); const cache = []; for (const key of keys) { const C = X(v.GetOwnProperty(key)) as Descriptor; if (C.Enumerable === Value.true) { cache.push([ key instanceof JSStringValue && bareKeyRe.test(key.stringValue()) ? key.stringValue() : i(key), C.Value ? i(C.Value) : '', ]); } } const tag = getObjectTag(v); let out = tag && tag !== 'Object' ? `${tag} {` : '{'; if (cache.length > 5) { cache.forEach((c) => { out = `${out}\n${' '.repeat(ctx.indent)}${c[0]}: ${c[1]},`; }); return `${out}\n${' '.repeat(ctx.indent - 1)}}`; } else { const oc = ctx.compact; ctx.compact = true; cache.forEach((c, index) => { out = `${out}${index === 0 ? '' : ','} ${c[0]}: ${c[1]}`; }); ctx.compact = oc; return `${out} }`; } } catch { return compactObject(ctx.realm, v); } finally { ctx.indent -= 1; ctx.inspected.pop(); } }, } satisfies Partial>; // TODO: add an option to inspect so it can return string with color. export function inspect(value: Value | ValueCompletion): string { const context: InspectContext = { realm: surroundingAgent.currentRealmRecord, indent: 0, inspected: [], compact: false, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const inner = (v: Value) => (INSPECTORS[v.type] as any)(v, context, inner); if (value instanceof Completion) { value = value.Value; } return inner(value); }