Skip to content

Commit

Permalink
feat: add ability to inspect prototype of non-plain objects
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Sep 6, 2024
1 parent 0d2a65d commit bddc2ce
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 17 deletions.
11 changes: 9 additions & 2 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* file that was distributed with this source code.
*/

import is from '@sindresorhus/is'
import type { Parser } from './parser.js'

const ObjectPrototype = Reflect.ownKeys(Object.prototype)
Expand All @@ -21,7 +22,7 @@ export function tokenizeObject(
config: {
depth: number
showHidden: boolean
inspectObjectPrototype: boolean
inspectObjectPrototype: boolean | 'unless-plain-object'
constructorName?: string
membersToIgnore?: (string | symbol)[]
eagerGetters?: (string | symbol)[]
Expand Down Expand Up @@ -131,7 +132,9 @@ export function tokenizeObject(
* Tokenize the prototype of an object. Prototypes should
* not count against the depth.
*/
if (config.inspectObjectPrototype) {
if (config.inspectObjectPrototype === true) {
tokenizePrototype(value, parser, { membersToIgnore: ObjectPrototype })
} else if (config.inspectObjectPrototype === 'unless-plain-object' && !is.plainObject(value)) {
tokenizePrototype(value, parser, { membersToIgnore: ObjectPrototype })
}

Expand All @@ -157,6 +160,10 @@ export function tokenizePrototype(
}
) {
const objectPrototype = Object.getPrototypeOf(value)
if (!objectPrototype) {
return
}

const ownKeys = Reflect.ownKeys(objectPrototype)
const eagerGetters = config.eagerGetters ?? []

Expand Down
2 changes: 1 addition & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class Parser {
this.config = Object.freeze({
showHidden: false,
depth: 5,
inspectObjectPrototype: false,
inspectObjectPrototype: 'unless-plain-object',
inspectArrayPrototype: false,
inspectStaticMembers: false,
maxArrayLength: 100,
Expand Down
1 change: 1 addition & 0 deletions src/tokenizers/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export const tokenizers: Partial<Record<TypeName, Tokenizer>> = {
tokenizeObject(value, parser, {
eagerGetters: ['message', 'stack'],
...parser.config,
inspectObjectPrototype: parser.config.inspectObjectPrototype === true ? true : false,
showHidden: true,
})
},
Expand Down
9 changes: 7 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,14 @@ export type ParserConfig = {
* Inspect prototype properties of an object. The non-enumerable properties
* of prototype are included by default.
*
* Defaults to false
* Defaults to "unless-plain-object"
*
* - The "unless-plain-object" will inspect the prototype of objects
* created with a prototype other than the "Object"
* - True will inspect the prototype for all objects that has a prototype
* - False will not inpsect the prototype
*/
inspectObjectPrototype?: boolean
inspectObjectPrototype?: boolean | 'unless-plain-object'

/**
* Inspect prototype properties of an array. The non-enumerable properties
Expand Down
181 changes: 169 additions & 12 deletions tests/parser/object.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,30 +599,31 @@ test.group('Parser | Object', () => {
{
"type": "object-value-end",
},
{
"type": "prototype-start",
},
{
"type": "prototype-end",
},
{
"type": "object-end",
},
]
`)
})

test('ignore prototype members even with showHidden', ({ expect }) => {
test('ignore prototype members with showHidden', ({ expect }) => {
const parser = new Parser({ showHidden: true })
class User {
id = 1
name = 'virk'
get username() {
return 'virk'
}
const user = {
id: 1,
name: 'virk',
}

const user = new User()
parser.parse(user)

expect(parser.flush()).toMatchInlineSnapshot(`
[
{
"constructorName": "User",
"constructorName": "Object",
"type": "object-start",
},
{
Expand Down Expand Up @@ -664,9 +665,9 @@ test.group('Parser | Object', () => {
`)
})

test('parse prototype members using inspectObjectPrototype', ({ expect }) => {
test('parse class prototype members using "unless-plain-object"', ({ expect }) => {
const parser = new Parser({
inspectObjectPrototype: true,
inspectObjectPrototype: 'unless-plain-object',
})
class User {
id = 1
Expand Down Expand Up @@ -761,6 +762,162 @@ test.group('Parser | Object', () => {
`)
})

test('do not parse plain object prototype with "unless-plain-object" option', ({ expect }) => {
const parser = new Parser({
inspectObjectPrototype: 'unless-plain-object',
})

const user = {
id: 1,
name: 'virk',
}
parser.parse(user)

expect(parser.flush()).toMatchInlineSnapshot(`
[
{
"constructorName": "Object",
"type": "object-start",
},
{
"isSymbol": false,
"isWritable": true,
"type": "object-key",
"value": "id",
},
{
"type": "object-value-start",
},
{
"type": "number",
"value": 1,
},
{
"type": "object-value-end",
},
{
"isSymbol": false,
"isWritable": true,
"type": "object-key",
"value": "name",
},
{
"type": "object-value-start",
},
{
"type": "string",
"value": "'virk'",
},
{
"type": "object-value-end",
},
{
"type": "object-end",
},
]
`)
})

test('parse plain object prototype when enabled inspectObjectPrototype', ({ expect }) => {
const parser = new Parser({
inspectObjectPrototype: true,
})

const user = {
id: 1,
name: 'virk',
}
parser.parse(user)

expect(parser.flush()).toMatchInlineSnapshot(`
[
{
"constructorName": "Object",
"type": "object-start",
},
{
"isSymbol": false,
"isWritable": true,
"type": "object-key",
"value": "id",
},
{
"type": "object-value-start",
},
{
"type": "number",
"value": 1,
},
{
"type": "object-value-end",
},
{
"isSymbol": false,
"isWritable": true,
"type": "object-key",
"value": "name",
},
{
"type": "object-value-start",
},
{
"type": "string",
"value": "'virk'",
},
{
"type": "object-value-end",
},
{
"type": "prototype-start",
},
{
"type": "prototype-end",
},
{
"type": "object-end",
},
]
`)
})

test('do not parse prototype of object with null prototype', ({ expect }) => {
const parser = new Parser({
inspectObjectPrototype: true,
})

const user = Object.create(null)
user.name = 'virk'
parser.parse(user)

expect(parser.flush()).toMatchInlineSnapshot(`
[
{
"constructorName": null,
"type": "object-start",
},
{
"isSymbol": false,
"isWritable": true,
"type": "object-key",
"value": "name",
},
{
"type": "object-value-start",
},
{
"type": "string",
"value": "'virk'",
},
{
"type": "object-value-end",
},
{
"type": "object-end",
},
]
`)
})

test('tokenize URL instance', ({ expect }) => {
const parser = new Parser()
parser.parse(new URL('foo', 'https://bar.com'))
Expand Down

0 comments on commit bddc2ce

Please sign in to comment.