Repository: node
Issue: util.inspect fails in some border conditions with an argument that has a `name`
Labels: util

### Version

v22.10.0

### Platform

```text
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:

```bash
working: 984
symbolName: 9
complexArrayName: 9
regExpName: 1
failRest: 0
```

```javascript
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`

```bash
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)

```bash
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`

```bash
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_