Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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