Skip to content

Commit de5dd5b

Browse files
gurgundayTony133
authored andcommitted
perf: reduce concatenation for optional properties
Signed-off-by: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent e02b5bb commit de5dd5b

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,17 @@ function buildInnerObject (context, location, objVar) {
502502
const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
503503
const defaultValue = propertyLocation.schema.default
504504
const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
505+
// Select a complete prefix so the comma does not need a separate concatenation.
506+
const propertyPrefix = sanitizedKey + ':'
507+
const addProperty = needsRuntimeComma
508+
? `json += addComma_${localUid} ? ${JSON.stringify(',' + propertyPrefix)} : ${JSON.stringify(propertyPrefix)}
509+
addComma_${localUid} = true`
510+
: `json += ${JSON.stringify(propertyPrefix)}`
505511

506512
code += `
507513
const ${value} = ${objVar}[${sanitizedKey}]
508514
if (${value} !== undefined) {
509-
${addComma}
510-
json += ${JSON.stringify(sanitizedKey + ':')}
515+
${addProperty}
511516
${buildValue(context, propertyLocation, `${value}`)}
512517
}`
513518

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use strict'
2+
3+
const { test } = require('node:test')
4+
const build = require('..')
5+
6+
function compile (schema, mode) {
7+
if (mode === 'normal') return build(schema)
8+
const output = { exports: {} }
9+
// eslint-disable-next-line no-new-func
10+
new Function('require', 'module', build(schema, { mode: 'standalone' }))(require, output)
11+
return output.exports
12+
}
13+
14+
for (const mode of ['normal', 'standalone']) {
15+
test('optional property prefixes preserve omissions and escaped keys: ' + mode, t => {
16+
const keys = ['first', 'quote"', 'back\\slash', 'line\nbreak']
17+
const properties = Object.fromEntries(keys.map(key => [key, { type: 'integer' }]))
18+
const stringify = compile({ type: 'object', properties, additionalProperties: false }, mode)
19+
20+
for (let mask = 0; mask < 16; mask++) {
21+
const input = { ignored: true }
22+
const expected = {}
23+
for (let i = keys.length - 1; i >= 0; i--) {
24+
input[keys[i]] = mask & (1 << i) ? i : undefined
25+
}
26+
for (let i = 0; i < keys.length; i++) {
27+
if (mask & (1 << i)) expected[keys[i]] = i
28+
}
29+
t.assert.equal(stringify(input), JSON.stringify(expected))
30+
}
31+
})
32+
33+
test('optional property prefixes share comma state with defaults and extra properties: ' + mode, t => {
34+
const stringify = compile({
35+
type: 'object',
36+
properties: {
37+
first: { type: 'string' },
38+
middle: { type: 'string', default: 'fallback' },
39+
last: { type: 'boolean' }
40+
},
41+
patternProperties: { '^x': { type: 'integer' } },
42+
additionalProperties: { type: 'string' }
43+
}, mode)
44+
45+
t.assert.equal(stringify({}), '{"middle":"fallback"}')
46+
t.assert.equal(stringify({ first: 'a', last: false, x: 2, extra: 'b' }), '{"first":"a","middle":"fallback","last":false,"x":2,"extra":"b"}')
47+
t.assert.equal(stringify({ middle: 'b', extra: 'c' }), '{"middle":"b","extra":"c"}')
48+
49+
const noDefaults = compile({
50+
type: 'object',
51+
properties: { first: { type: 'string' } },
52+
additionalProperties: true
53+
}, mode)
54+
t.assert.equal(noDefaults({ extra: 1 }), '{"extra":1}')
55+
t.assert.equal(noDefaults({ first: 'a', extra: 1 }), '{"first":"a","extra":1}')
56+
})
57+
58+
test('optional property prefixes reset for nested objects and array items: ' + mode, t => {
59+
const item = {
60+
type: 'object',
61+
properties: { first: { type: 'integer' }, last: { type: 'integer' } }
62+
}
63+
const stringify = compile({
64+
type: 'object',
65+
properties: {
66+
before: item,
67+
items: { type: 'array', items: item },
68+
after: item
69+
}
70+
}, mode)
71+
const input = {
72+
before: {},
73+
items: [{ last: 1 }, {}, { first: 2, last: 3 }, { first: 4 }],
74+
after: { last: 5 }
75+
}
76+
t.assert.equal(stringify(input), JSON.stringify(input))
77+
t.assert.equal(stringify({ after: { first: 6 } }), '{"after":{"first":6}}')
78+
})
79+
80+
test('optional property prefixes preserve getter and coercion order: ' + mode, t => {
81+
const stringify = compile({
82+
type: 'object',
83+
properties: {
84+
first: { type: 'string' },
85+
second: { type: 'integer' },
86+
last: { type: 'boolean' }
87+
}
88+
}, mode)
89+
const events = []
90+
const failure = new Error('coercion failed')
91+
let fail = false
92+
const input = {
93+
get first () {
94+
events.push('first')
95+
return {
96+
toString () {
97+
events.push('coerce first')
98+
if (fail) throw failure
99+
return 'a'
100+
}
101+
}
102+
},
103+
get second () {
104+
events.push('second')
105+
return 2
106+
},
107+
get last () {
108+
events.push('last')
109+
return undefined
110+
}
111+
}
112+
t.assert.equal(stringify(input), '{"first":"a","second":2}')
113+
t.assert.deepStrictEqual(events, ['first', 'coerce first', 'second', 'last'])
114+
events.length = 0
115+
fail = true
116+
t.assert.throws(() => stringify(input), error => error === failure)
117+
t.assert.deepStrictEqual(events, ['first', 'coerce first'])
118+
})
119+
}

0 commit comments

Comments
 (0)