Skip to content

Commit 24884f6

Browse files
committed
Address short circuiting in and / or interpreted
1 parent 91d1820 commit 24884f6

File tree

4 files changed

+125
-31
lines changed

4 files changed

+125
-31
lines changed

compiler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ function processBuiltString (method, str, buildState) {
314314
return Object.assign(
315315
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray), {
316316
[Sync]: !buildState.asyncDetected,
317-
aboveDetected: str.includes(', above')
317+
aboveDetected: typeof str === 'string' && str.includes(', above')
318318
})
319319
}
320320

defaultMethods.js

+73-29
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,81 @@ const defaultMethods = {
159159
'!=': ([a, b]) => a != b,
160160
'!==': ([a, b]) => a !== b,
161161
xor: ([a, b]) => a ^ b,
162-
or: (arr, _1, _2, engine) => {
163-
for (let i = 0; i < arr.length; i++) {
164-
if (engine.truthy(arr[i])) return arr[i]
165-
}
166-
return arr[arr.length - 1]
162+
// Why "executeInLoop"? Because if it needs to execute to get an array, I do not want to execute the arguments,
163+
// Both for performance and safety reasons.
164+
or: {
165+
method: (arr, _1, _2, engine) => {
166+
// See "executeInLoop" above
167+
const executeInLoop = Array.isArray(arr)
168+
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
169+
170+
let item
171+
for (let i = 0; i < arr.length; i++) {
172+
item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
173+
if (engine.truthy(item)) return item
174+
}
175+
176+
return item
177+
},
178+
asyncMethod: async (arr, _1, _2, engine) => {
179+
// See "executeInLoop" above
180+
const executeInLoop = Array.isArray(arr)
181+
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
182+
183+
let item
184+
for (let i = 0; i < arr.length; i++) {
185+
item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
186+
if (engine.truthy(item)) return item
187+
}
188+
189+
return item
190+
},
191+
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
192+
compile: (data, buildState) => {
193+
if (!buildState.engine.truthy.IDENTITY) return false
194+
if (Array.isArray(data)) {
195+
return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
196+
} else {
197+
return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
198+
}
199+
},
200+
traverse: false
167201
},
168-
and: (arr, _1, _2, engine) => {
169-
for (let i = 0; i < arr.length; i++) {
170-
if (!engine.truthy(arr[i])) return arr[i]
202+
and: {
203+
method: (arr, _1, _2, engine) => {
204+
// See "executeInLoop" above
205+
const executeInLoop = Array.isArray(arr)
206+
if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 })
207+
208+
let item
209+
for (let i = 0; i < arr.length; i++) {
210+
item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i]
211+
if (!engine.truthy(item)) return item
212+
}
213+
return item
214+
},
215+
asyncMethod: async (arr, _1, _2, engine) => {
216+
// See "executeInLoop" above
217+
const executeInLoop = Array.isArray(arr)
218+
if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 })
219+
220+
let item
221+
for (let i = 0; i < arr.length; i++) {
222+
item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i]
223+
if (!engine.truthy(item)) return item
224+
}
225+
return item
226+
},
227+
traverse: false,
228+
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
229+
compile: (data, buildState) => {
230+
if (!buildState.engine.truthy.IDENTITY) return false
231+
if (Array.isArray(data)) {
232+
return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
233+
} else {
234+
return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
235+
}
171236
}
172-
return arr[arr.length - 1]
173237
},
174238
substr: ([string, from, end]) => {
175239
if (end < 0) {
@@ -677,32 +741,12 @@ defaultMethods['%'].compile = function (data, buildState) {
677741
}
678742
}
679743

680-
// @ts-ignore Allow custom attribute
681-
defaultMethods.or.compile = function (data, buildState) {
682-
if (!buildState.engine.truthy.IDENTITY) return false
683-
if (Array.isArray(data)) {
684-
return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
685-
} else {
686-
return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
687-
}
688-
}
689-
690744
// @ts-ignore Allow custom attribute
691745
defaultMethods.in.compile = function (data, buildState) {
692746
if (!Array.isArray(data)) return false
693747
return buildState.compile`(${data[1]} || []).includes(${data[0]})`
694748
}
695749

696-
// @ts-ignore Allow custom attribute
697-
defaultMethods.and.compile = function (data, buildState) {
698-
if (!buildState.engine.truthy.IDENTITY) return false
699-
if (Array.isArray(data)) {
700-
return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
701-
} else {
702-
return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
703-
}
704-
}
705-
706750
// @ts-ignore Allow custom attribute
707751
defaultMethods['-'].compile = function (data, buildState) {
708752
if (Array.isArray(data)) {

general.test.js

+50
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,56 @@ describe('Various Test Cases', () => {
198198
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { map: [[1, 2, 3], { map: [[1], { var: '../../../../name' }] }] }, { name: 'Bob' }, [['Bob'], ['Bob'], ['Bob']])
199199
})
200200

201+
it('correctly short circuits (or)', async () => {
202+
for (const engine of [...normalEngines, ...permissiveEngines]) {
203+
let count = 0
204+
205+
engine.addMethod('increment', {
206+
method: () => {
207+
count++
208+
return true
209+
}
210+
})
211+
await testEngine(engine, { or: [{ increment: true }, { increment: true }, { increment: true }] }, {}, true)
212+
// It's called twice because it runs once for interpreted, and once for compiled.
213+
assert.strictEqual(count, 2, 'Should have only called increment twice. (Count)')
214+
}
215+
})
216+
217+
it('correctly short circuits (and)', async () => {
218+
for (const engine of [...normalEngines, ...permissiveEngines]) {
219+
let count = 0
220+
221+
engine.addMethod('increment', {
222+
method: () => {
223+
count++
224+
return false
225+
}
226+
})
227+
await testEngine(engine, { and: [{ increment: true }, { increment: true }, { increment: true }] }, {}, false)
228+
// It's called twice because it runs once for interpreted, and once for compiled.
229+
assert.strictEqual(count, 2, 'Should have only called increment twice. (Count)')
230+
}
231+
})
232+
233+
it('does not execute any logic from and / or array if the argument is executed', async () => {
234+
for (const engine of [...normalEngines, ...permissiveEngines]) {
235+
let count = 0
236+
engine.addMethod('increment', {
237+
method: () => {
238+
count++
239+
return true
240+
}
241+
})
242+
243+
await testEngine(engine, { and: { preserve: [{ increment: true }, { increment: true }] } }, {}, { increment: true })
244+
assert.strictEqual(count, 0, 'Should not have called increment on and.')
245+
246+
await testEngine(engine, { or: { preserve: [{ increment: true }, { increment: true }] } }, {}, { increment: true })
247+
assert.strictEqual(count, 0, 'Should not have called increment on or.')
248+
}
249+
})
250+
201251
it('disables interpreted optimization when it realizes it will not be faster', async () => {
202252
for (const engine of [...normalEngines, ...permissiveEngines]) {
203253
const disableInterpretedOptimization = engine.disableInterpretedOptimization

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-logic-engine",
3-
"version": "4.0.1",
3+
"version": "4.0.2",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

0 commit comments

Comments
 (0)