Skip to content

util.inspect fails in some border conditions with an argument that has a name #56570

@jandockx

Description

@jandockx

Version

v22.10.0

Platform

Darwin XXXX.lan 23.6.0 Darwin Kernel Version 23.6.0: Wed Jul 31 20:48:44 PDT 2024; root:xnu-10063.141.1.700.5~1/RELEASE_X86_64 x86_64 

Subsystem

node:util

What steps will reproduce the bug?

util.inspect, as I understand it, is supposed to work in all conditions, whatever we throw at it. The code below shows 19 cases where util.inspect fails:

working: 984 symbolName: 9 complexArrayName: 9 regExpName: 1 failRest: 0
import { inspect } from 'node:util' import { strictEqual, notStrictEqual } from 'node:assert' function setAndFreeze(obj, propertyName, value) { Object.defineProperty(obj, propertyName, { configurable: false, enumerable: true, writable: false, value }) return obj } function buildPrimitiveStuff() { const base = [ { subject: undefined, description: 'undefined' }, { subject: 'abc', description: 'string' }, { subject: '', description: 'empty string' }, { subject: 0, description: 'zero (0)' }, { subject: 1, description: 'one (1)' }, { subject: -1, description: 'minus one (-1)' }, { subject: 4, description: 'natural' }, { subject: -456, description: 'negative integer' }, { subject: Math.PI, description: 'positive float' }, { subject: -Math.E, description: 'negative float' }, { subject: 0.1, description: 'non-binary float' }, // 10 * 0.1 !== 1 { subject: Number.POSITIVE_INFINITY, description: 'positive infinity' }, { subject: Number.NEGATIVE_INFINITY, description: 'negative infinity' }, { subject: Number.MAX_SAFE_INTEGER, description: 'max safe integer' }, { subject: Number.MIN_SAFE_INTEGER, description: 'min safe integer' }, { subject: Number.MAX_VALUE, description: 'max number' }, { subject: Number.MIN_VALUE, description: 'min number' }, { subject: Number.EPSILON, description: 'epsilon' }, { subject: Number.NaN, description: 'NaN' }, { subject: false, description: 'false' }, { subject: true, description: 'true' }, { subject: Symbol('isolated symbol as stuff'), description: 'symbol' }, { subject: 0n, description: 'zero bigint (0n)' }, { subject: 1n, description: 'one bigint (1n)' }, { subject: -1n, description: 'minus one bigint (-1n)' }, { subject: 123456789012345678901234567890n, description: 'large positive bigint' }, { subject: -123456789012345678901234567890n, description: 'large negative bigint' }, { subject: BigInt(Number.MAX_SAFE_INTEGER), description: 'bigint equivalent of max safe integer' }, { subject: BigInt(Number.MIN_SAFE_INTEGER), description: 'bigint equivalent of min safe integer' } ] return base.map(s => ({ ...s, primitive: true, mutable: false })) } export const primitiveStuff = buildPrimitiveStuff() export const primitiveAndNullStuff = [ { subject: null, description: 'null', primitive: false, mutable: false }, ...primitiveStuff ] export function buildMutableStuffGenerators() { const base = [ { generate: () => new Error('This is an error case'), description: 'Error' }, { generate: () => new Date(2025, 0, 11, 14, 11, 48, 857), description: 'Date' }, { generate: () => /foo/, description: 'RegExp' }, { generate: () => function () { return 'an anonymous function' }, description: 'anonymous function' }, { generate: () => function namedFunction() { return 'a named function' }, description: 'named function' }, { generate: () => () => 'an arrow function', description: 'arrow function' }, { generate: () => { const arrowFunction = () => 'an arrow function in a const' return arrowFunction }, description: 'arrow function in a const' }, { generate: () => async function () { return 'an anonymous async function' }, description: 'anonymous async function' }, { generate: () => async function asyncNamedFunction() { return 'an async named function' }, description: 'async named function' }, { generate: () => async () => 'an async arrow function', description: 'async arrow function' }, { generate: () => { const asyncArrowFunction = async () => 'an async arrow function in a const' return asyncArrowFunction }, description: 'async arrow function in a const' }, // eslint-disable-next-line no-new-wrappers { generate: () => new Number(42), description: 'Number' }, // eslint-disable-next-line no-new-wrappers { generate: () => new Boolean(false), description: 'Boolean' }, // eslint-disable-next-line no-new-wrappers { generate: () => new String('string wrapper object'), description: 'String' }, // NOTE: IArguments already has a frozen name `null` (at least in Node) ??!??!!? { generate: () => arguments, description: 'arguments object' }, { generate: () => ({}), description: 'empty object' }, { generate: () => ({ a: 1, b: 'b', c: {}, d: { d1: undefined, d2: 'd2', d3: { d31: 31 } }, e: [5, 'c', true], f: Symbol('f') }), description: 'complex object' }, { generate: () => [], description: 'empty array' }, { generate: () => [4, 'z', { a: 'a' }, true, ['b', 12]], description: 'simple array' } // no Symbols // MUDO add circular stuff ] const arrayWithAllInIt = { generate: () => [...primitiveAndNullStuff.map(({ subject }) => subject), ...base.map(({ generate }) => generate())], description: 'complex array', primitive: false, mutable: true } return [...base.map(sg => ({ ...sg, primitive: false, mutable: true })), arrayWithAllInIt] } export const mutableStuffGenerators = buildMutableStuffGenerators() export const stuffGenerators = [ ...primitiveAndNullStuff.map(({ subject, description, primitive, mutable }) => ({ generate: () => subject, description, primitive, mutable })), ...buildMutableStuffGenerators() ] const namedStuff = mutableStuffGenerators.reduce(function addArrayOfNamedSubjectsToAcc( acc, { generate: generateSubject, description: subjectDescription } ) { if (subjectDescription === 'arguments object') { return [ ...acc, { subject: generateSubject(), // NOTE: IArguments already has a frozen name `null` ??!??!!? description: `${subjectDescription} with an already frozen name \`null\`` } ] } return [ ...acc, ...stuffGenerators.map(function addFrozenNameToSubject({ generate: generateName, description: nameDescription }) { return { subject: setAndFreeze(generateSubject(), 'name', generateName()), description: `${subjectDescription} with ${nameDescription} name`, fail: ((subjectDescription === 'Error' || subjectDescription.includes('function')) && nameDescription === 'symbol') || ((subjectDescription === 'Error' || subjectDescription.includes('function')) && nameDescription === 'complex array') || (subjectDescription === 'Error' && nameDescription === 'RegExp') ? { subject: subjectDescription, name: nameDescription } : undefined } }) ] }, []) function generateMultiLineAnonymousFunction() { return function () { // NOTE: string in place to make the _source_ multi-line // trim: spaces at start let x = ' This is a multi-line function' x += 'The intention of this test' x += 'is to verify' // start of white line // end of white line x += 'whether we get an acceptable' x += 'is to shortened version of this' x += 'as a concise representation' x += 'this function should have no name ' // trim return x } } const stuff = [ ...stuffGenerators.map(({ generate, description }) => ({ subject: generate(), description })), ...namedStuff, { subject: generateMultiLineAnonymousFunction(), description: 'multi-line anonymous function' }, { subject: setAndFreeze( generateMultiLineAnonymousFunction(), 'name', ` This is a multi-line name The intention of this test is to verify  whether we get an acceptable is to shortened version of this as a concise representation this function should have a name ` // trim ), description: 'multi-line anonymous function with frozen name' } ] const triage = stuff.reduce( (acc, s) => { if (s.fail) { if (s.fail.name === 'symbol') { acc.symbolName.push(s) } else if (s.fail.name === 'complex array') { acc.complexArrayName.push(s) } else if (s.fail.name === 'RegExp') { acc.regExpName.push(s) } else { acc.failRest.push(s) } } else { acc.working.push(s) } return acc }, { working: [], symbolName: [], complexArrayName: [], regExpName: [], failRest: [] } ) describe('node util.inspect', function () { describe('working as expected', function () { console.info(`working: ${triage.working.length}`) triage.working.forEach(({ subject, description }) => { it(`works for a ${description}`, function () { const result = inspect(subject) strictEqual(typeof result, 'string') notStrictEqual(result, '') }) }) }) describe('failing with a symbol name', function () { console.info(`symbolName: ${triage.symbolName.length}`) triage.symbolName.forEach(({ subject, description }) => { it(`fails for a ${description}`, function () { const result = inspect(subject) strictEqual(typeof result, 'string') notStrictEqual(result, '') }) }) }) describe('failing with a complex array name', function () { console.info(`complexArrayName: ${triage.complexArrayName.length}`) triage.complexArrayName.forEach(({ subject, description }) => { it(`fails for a ${description}`, function () { const result = inspect(subject) strictEqual(typeof result, 'string') notStrictEqual(result, '') }) }) }) describe('failing with a RegExp name', function () { console.info(`regExpName: ${triage.regExpName.length}`) triage.regExpName.forEach(({ subject, description }) => { it(`fails for a ${description}`, function () { const result = inspect(subject) strictEqual(typeof result, 'string') notStrictEqual(result, '') }) }) }) describe('other failing', function () { console.info(`failRest: ${triage.failRest.length}`) triage.failRest.forEach(({ subject, description }) => { it(`fails for a ${description}`, function () { const result = inspect(subject) strictEqual(typeof result, 'string') notStrictEqual(result, '') }) }) }) })

How often does it reproduce? Is there a required condition?

Reproducible with the above code.

What is the expected behavior? Why is that the expected behavior?

util.inspect, as I understand it, is supposed to work and return a string in all conditions, whatever we throw at it.

What do you see instead?

symbolName

symbolName represents 9 failing cases, where the object argument to util.inspect is an Error or a function, with a symbol as a name

TypeError: Cannot convert a Symbol value to a string at getFunctionBase (node:internal/util/inspect:1208:24) at formatRaw (node:internal/util/inspect:962:14) at formatValue (node:internal/util/inspect:844:10) at inspect (node:internal/util/inspect:368:10) at Context.<anonymous> (file:///Users/XXXXXX/util.inspect.test.js:277:24) at process.processImmediate (node:internal/timers:491:21)

complexArrayName

complexArrayName represents 9 failing cases, where the object argument to util.inspect is an Error or a function, with an array that contains a symbol as a name (note that using an object with a property that has a symbol value works as expected)

TypeError: Cannot convert a Symbol value to a string at Array.join (<anonymous>) at Array.toString (<anonymous>) at String (<anonymous>) at formatError (node:internal/util/inspect:1372:35) at formatRaw (node:internal/util/inspect:989:14) at formatValue (node:internal/util/inspect:844:10) at inspect (node:internal/util/inspect:368:10) at Context.<anonymous> (file:///Users/XXXXXX/util.inspect.test.js:287:24) at process.processImmediate (node:internal/timers:491:21)

regExpName

regExpName represents 1 failing case, where the object argument to util.inspect is an Error with a RegExp as a name

TypeError: First argument to String.prototype.includes must not be a regular expression at String.includes (<anonymous>) at removeDuplicateErrorKeys (node:internal/util/inspect:1312:27) at formatError (node:internal/util/inspect:1375:3) at formatRaw (node:internal/util/inspect:989:14) at formatValue (node:internal/util/inspect:844:10) at inspect (node:internal/util/inspect:368:10) at Context.<anonymous> (file:///Users/XXXXXX/util.inspect.test.js:297:24) at process.processImmediate (node:internal/timers:491:21)

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    utilIssues and PRs related to the built-in util module.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions