Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jandockx opened this issue Jan 12, 2025 · 3 comments
Open
Labels
util Issues and PRs related to the built-in util module.

Comments

@jandockx
Copy link

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

@ljharb
Copy link
Member

ljharb commented Jan 12, 2025

#56572 should address symbolName, #56573 some of complexArrayName.

@ljharb
Copy link
Member

ljharb commented Jan 12, 2025

#56574 should address regExpName

@ljharb ljharb added the util Issues and PRs related to the built-in util module. label Jan 12, 2025
@jandockx
Copy link
Author

jandockx commented Jan 12, 2025

@ljharb ☀️

ljharb added a commit to ljharb/node that referenced this issue Jan 13, 2025
ljharb added a commit to ljharb/node that referenced this issue Jan 13, 2025
ljharb added a commit to ljharb/node that referenced this issue Jan 13, 2025
aduh95 pushed a commit that referenced this issue Jan 15, 2025
Refs: #56570
PR-URL: #56572
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
ljharb added a commit to ljharb/node that referenced this issue Jan 15, 2025
ljharb added a commit to ljharb/node that referenced this issue Jan 15, 2025
nodejs-github-bot pushed a commit that referenced this issue Jan 15, 2025
See #56570

PR-URL: #56574
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Matthew Aitken <[email protected]>
Reviewed-By: Marco Ippolito <[email protected]>
Reviewed-By: Luigi Pinca <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
util Issues and PRs related to the built-in util module.
Projects
None yet
Development

No branches or pull requests

2 participants