• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TotalTechGeek / json-logic-engine / 12243740391

09 Dec 2024 08:28PM UTC coverage: 92.265% (+0.01%) from 92.254%
12243740391

Pull #39

github

web-flow
Merge 2960e6914 into 91d18201d
Pull Request #39: Separate out the val proposal

838 of 947 branches covered (88.49%)

Branch coverage included in aggregate %.

44 of 45 new or added lines in 1 file covered. (97.78%)

20 existing lines in 3 files now uncovered.

832 of 863 relevant lines covered (96.41%)

30698.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.56
/defaultMethods.js
1
// @ts-check
2
'use strict'
3

4
import asyncIterators from './async_iterators.js'
5
import { Sync, isSync } from './constants.js'
6
import declareSync from './utilities/declareSync.js'
7
import { build, buildString } from './compiler.js'
8
import chainingSupported from './utilities/chainingSupported.js'
9
import InvalidControlInput from './errors/InvalidControlInput.js'
10
import { splitPathMemoized } from './utilities/splitPath.js'
11

12
function isDeterministic (method, engine, buildState) {
13
  if (Array.isArray(method)) {
25,824✔
14
    return method.every((i) => isDeterministic(i, engine, buildState))
16,368✔
15
  }
16
  if (method && typeof method === 'object') {
20,136✔
17
    const func = Object.keys(method)[0]
5,982✔
18
    const lower = method[func]
5,982✔
19

20
    if (engine.isData(method, func)) return true
5,982✔
21
    if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
5,898!
22

23
    if (engine.methods[func].traverse === false) {
5,898✔
24
      return typeof engine.methods[func].deterministic === 'function'
216!
25
        ? engine.methods[func].deterministic(lower, buildState)
26
        : engine.methods[func].deterministic
27
    }
28
    return typeof engine.methods[func].deterministic === 'function'
5,682✔
29
      ? engine.methods[func].deterministic(lower, buildState)
30
      : engine.methods[func].deterministic &&
3,066✔
31
          isDeterministic(lower, engine, buildState)
32
  }
33
  return true
14,154✔
34
}
35

36
function isSyncDeep (method, engine, buildState) {
37
  if (Array.isArray(method)) {
26,964✔
38
    return method.every((i) => isSyncDeep(i, engine, buildState))
14,892✔
39
  }
40

41
  if (method && typeof method === 'object') {
21,192✔
42
    const func = Object.keys(method)[0]
8,688✔
43
    const lower = method[func]
8,688✔
44
    if (engine.isData(method, func)) return true
8,688✔
45
    if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
8,646!
46
    if (engine.methods[func].traverse === false) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync]
8,646!
47
    return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] && isSyncDeep(lower, engine, buildState)
5,574!
48
  }
49

50
  return true
12,504✔
51
}
52

53
const defaultMethods = {
54✔
54
  '+': (data) => {
55
    if (typeof data === 'string') return +data
483,444✔
56
    if (typeof data === 'number') return +data
483,432!
57
    let res = 0
483,432✔
58
    for (let i = 0; i < data.length; i++) res += +data[i]
966,852✔
59
    return res
483,432✔
60
  },
61
  '*': (data) => {
62
    let res = 1
864✔
63
    for (let i = 0; i < data.length; i++) res *= +data[i]
1,752✔
64
    return res
864✔
65
  },
66
  '/': (data) => {
67
    let res = data[0]
300✔
68
    for (let i = 1; i < data.length; i++) res /= +data[i]
324✔
69
    return res
300✔
70
  },
71
  '-': (data) => {
72
    if (typeof data === 'string') return -data
516✔
73
    if (typeof data === 'number') return -data
504!
74
    if (data.length === 1) return -data[0]
504✔
75
    let res = data[0]
264✔
76
    for (let i = 1; i < data.length; i++) res -= +data[i]
288✔
77
    return res
264✔
78
  },
79
  '%': (data) => {
80
    let res = data[0]
504✔
81
    for (let i = 1; i < data.length; i++) res %= +data[i]
528✔
82
    return res
504✔
83
  },
84
  max: (data) => Math.max(...data),
732✔
85
  min: (data) => Math.min(...data),
336✔
86
  in: ([item, array]) => (array || []).includes(item),
456✔
87
  '>': ([a, b]) => a > b,
1,452✔
88
  '<': ([a, b, c]) => (c === undefined ? a < b : a < b && b < c),
1,680✔
89
  preserve: {
90
    traverse: false,
91
    method: declareSync((i) => i, true),
1,626✔
92
    [Sync]: () => true
462✔
93
  },
94
  if: {
95
    method: (input, context, above, engine) => {
96
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
3,234!
97

98
      if (input.length === 1) return engine.run(input[0], context, { above })
3,234✔
99
      if (input.length < 2) return null
3,006✔
100

101
      input = [...input]
2,922✔
102
      if (input.length % 2 !== 1) input.push(null)
2,922✔
103

104
      // fallback to the default if the condition is false
105
      const onFalse = input.pop()
2,922✔
106

107
      // while there are still conditions
108
      while (input.length) {
2,922✔
109
        const check = input.shift()
3,678✔
110
        const onTrue = input.shift()
3,678✔
111

112
        const test = engine.run(check, context, { above })
3,678✔
113

114
        // if the condition is true, run the true branch
115
        if (engine.truthy(test)) return engine.run(onTrue, context, { above })
3,678✔
116
      }
117

118
      return engine.run(onFalse, context, { above })
942✔
119
    },
120
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
3,612✔
121
    deterministic: (data, buildState) => {
122
      return isDeterministic(data, buildState.engine, buildState)
3,216✔
123
    },
124
    asyncMethod: async (input, context, above, engine) => {
125
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
102!
126

127
      // check the bounds
128
      if (input.length === 1) return engine.run(input[0], context, { above })
102✔
129
      if (input.length < 2) return null
66✔
130

131
      input = [...input]
54✔
132

133
      if (input.length % 2 !== 1) input.push(null)
54✔
134

135
      // fallback to the default if the condition is false
136
      const onFalse = input.pop()
54✔
137

138
      // while there are still conditions
139
      while (input.length) {
54✔
140
        const check = input.shift()
54✔
141
        const onTrue = input.shift()
54✔
142

143
        const test = await engine.run(check, context, { above })
54✔
144

145
        // if the condition is true, run the true branch
146
        if (engine.truthy(test)) return engine.run(onTrue, context, { above })
54✔
147
      }
148

149
      return engine.run(onFalse, context, { above })
18✔
150
    },
151
    traverse: false
152
  },
153
  '<=': ([a, b, c]) => (c === undefined ? a <= b : a <= b && b <= c),
696✔
154
  '>=': ([a, b]) => a >= b,
1,380✔
155
  // eslint-disable-next-line eqeqeq
156
  '==': ([a, b]) => a == b,
840✔
157
  '===': ([a, b]) => a === b,
1,608✔
158
  // eslint-disable-next-line eqeqeq
159
  '!=': ([a, b]) => a != b,
516✔
160
  '!==': ([a, b]) => a !== b,
576✔
161
  xor: ([a, b]) => a ^ b,
264✔
162
  or: (arr, _1, _2, engine) => {
163
    for (let i = 0; i < arr.length; i++) {
1,584✔
164
      if (engine.truthy(arr[i])) return arr[i]
2,556✔
165
    }
166
    return arr[arr.length - 1]
312✔
167
  },
168
  and: (arr, _1, _2, engine) => {
169
    for (let i = 0; i < arr.length; i++) {
2,112✔
170
      if (!engine.truthy(arr[i])) return arr[i]
3,564✔
171
    }
172
    return arr[arr.length - 1]
852✔
173
  },
174
  substr: ([string, from, end]) => {
175
    if (end < 0) {
720✔
176
      const result = string.substr(from)
192✔
177
      return result.substr(0, result.length + end)
192✔
178
    }
179
    return string.substr(from, end)
528✔
180
  },
181
  length: ([i]) => {
182
    if (typeof i === 'string' || Array.isArray(i)) return i.length
144✔
183
    if (i && typeof i === 'object') return Object.keys(i).length
96✔
184
    return 0
24✔
185
  },
186
  get: {
187
    method: ([data, key, defaultValue], context, above, engine) => {
188
      const notFound = defaultValue === undefined ? null : defaultValue
480✔
189

190
      const subProps = splitPathMemoized(String(key))
480✔
191
      for (let i = 0; i < subProps.length; i++) {
480✔
192
        if (data === null || data === undefined) {
480!
193
          return notFound
×
194
        }
195
        // Descending into context
196
        data = data[subProps[i]]
480✔
197
        if (data === undefined) {
480✔
198
          return notFound
48✔
199
        }
200
      }
201
      if (engine.allowFunctions || typeof data[key] !== 'function') {
432!
202
        return data
432✔
203
      }
204
    }
205
  },
206
  // Adding this to spec something out, not to merge it quite yet
207
  val: {
208
    method: (args, context, above) => {
209
      if (Array.isArray(args) && args.length === 1) args = args[0]
1,728✔
210
      if (!Array.isArray(args)) {
1,728✔
211
        if (args === null || args === undefined) return context
912✔
212
        const result = context[args]
144✔
213
        if (typeof result === 'undefined') return null
144!
214
        return result
144✔
215
      }
216
      let result = context
816✔
217
      let start = 0
816✔
218
      if (Array.isArray(args[0]) && args[0].length === 1) {
816✔
219
        start++
720✔
220
        const climb = +Math.abs(args[0][0])
720✔
221
        let pos = 0
720✔
222
        for (let i = 0; i < climb; i++) {
720✔
223
          result = above[pos++]
1,872✔
224
          if (i === above.length - 1 && Array.isArray(result)) {
1,872✔
225
            above = result
288✔
226
            result = result[0]
288✔
227
            pos = 1
288✔
228
          }
229
        }
230
      }
231
      for (let i = start; i < args.length; i++) {
816✔
232
        if (args[i] === null) continue
960!
233
        if (result === null || result === undefined) return null
960!
234
        result = result[args[i]]
960✔
235
      }
236
      if (typeof result === 'undefined') return null
816!
237
      return result
816✔
238
    },
239
    optimizeUnary: true,
240
    deterministic: (data, buildState) => {
241
      if (buildState.insideIterator) {
1,224✔
242
        if (Array.isArray(data) && Array.isArray(data[0]) && Math.abs(data[0][0]) >= 2) return false
480✔
243
        return true
336✔
244
      }
245
      return false
744✔
246
    },
247
    compile: (data, buildState) => {
248
      function wrapNull (data) {
249
        if (!chainingSupported) return buildState.compile`(((a) => a === null || a === undefined ? null : a)(${data}))`
456✔
250
        return buildState.compile`(${data} ?? null)`
380✔
251
      }
252
      if (Array.isArray(data) && Array.isArray(data[0])) {
576✔
253
        // A very, very specific optimization.
254
        if (buildState.iteratorCompile && Math.abs(data[0][0] || 0) === 1 && data[1] === 'index') return buildState.compile`index`
120!
255
        return false
96✔
256
      }
257
      if (Array.isArray(data) && data.length === 1) data = data[0]
456✔
258
      if (data === null) return wrapNull(buildState.compile`context`)
456✔
259
      if (!Array.isArray(data)) return wrapNull(buildState.compile`context[${data}]`)
240✔
260
      if (Array.isArray(data)) {
96!
261
        let res = buildState.compile`context`
96✔
262
        for (let i = 0; i < data.length; i++) {
96✔
263
          if (data[i] === null) continue
240!
264
          if (chainingSupported) res = buildState.compile`${res}?.[${data[i]}]`
240✔
265
          else res = buildState.compile`(${res}|| 0)[${data[i]}]`
40✔
266
        }
267
        return wrapNull(buildState.compile`(${res})`)
96✔
268
      }
NEW
UNCOV
269
      return false
×
270
    }
271
  },
272
  var: (key, context, above, engine) => {
273
    let b
274
    if (Array.isArray(key)) {
22,362✔
275
      b = key[1]
16,392✔
276
      key = key[0]
16,392✔
277
    }
278
    let iter = 0
22,362✔
279
    while (
22,362✔
280
      typeof key === 'string' &&
62,976✔
281
      key.startsWith('../') &&
282
      iter < above.length
283
    ) {
284
      context = above[iter++]
6,132✔
285
      key = key.substring(3)
6,132✔
286
      // A performance optimization that allows you to pass the previous above array without spreading it as the last argument
287
      if (iter === above.length && Array.isArray(context)) {
6,132✔
288
        iter = 0
288✔
289
        above = context
288✔
290
        context = above[iter++]
288✔
291
      }
292
    }
293

294
    const notFound = b === undefined ? null : b
22,362✔
295
    if (typeof key === 'undefined' || key === '' || key === null) {
22,362✔
296
      if (engine.allowFunctions || typeof context !== 'function') {
3,096!
297
        return context
3,096✔
298
      }
UNCOV
299
      return null
×
300
    }
301
    const subProps = splitPathMemoized(String(key))
19,266✔
302
    for (let i = 0; i < subProps.length; i++) {
19,266✔
303
      if (context === null || context === undefined) {
21,690✔
304
        return notFound
96✔
305
      }
306
      // Descending into context
307
      context = context[subProps[i]]
21,594✔
308
      if (context === undefined) {
21,594✔
309
        return notFound
3,144✔
310
      }
311
    }
312
    if (engine.allowFunctions || typeof context !== 'function') {
16,026✔
313
      return context
15,954✔
314
    }
315
    return null
72✔
316
  },
317
  missing: (checked, context, above, engine) => {
318
    return (Array.isArray(checked) ? checked : [checked]).filter((key) => {
2,736!
319
      return defaultMethods.var(key, context, above, engine) === null
4,656✔
320
    })
321
  },
322
  missing_some: ([needCount, options], context, above, engine) => {
323
    const missing = defaultMethods.missing(options, context, above, engine)
864✔
324
    if (options.length - missing.length >= needCount) {
864✔
325
      return []
576✔
326
    } else {
327
      return missing
288✔
328
    }
329
  },
330
  map: createArrayIterativeMethod('map'),
331
  some: createArrayIterativeMethod('some', true),
332
  all: createArrayIterativeMethod('every', true),
333
  none: {
334
    traverse: false,
335
    // todo: add async build & build
336
    method: (val, context, above, engine) => {
337
      return !defaultMethods.some.method(val, context, above, engine)
192✔
338
    },
339
    asyncMethod: async (val, context, above, engine) => {
340
      return !(await defaultMethods.some.asyncMethod(
192✔
341
        val,
342
        context,
343
        above,
344
        engine
345
      ))
346
    },
347
    compile: (data, buildState) => {
348
      const result = defaultMethods.some.compile(data, buildState)
384✔
349
      return result ? buildState.compile`!(${result})` : false
384!
350
    }
351
  },
352
  merge: (arrays) => (Array.isArray(arrays) ? [].concat(...arrays) : [arrays]),
1,140!
353
  every: createArrayIterativeMethod('every'),
354
  filter: createArrayIterativeMethod('filter'),
355
  reduce: {
356
    deterministic: (data, buildState) => {
357
      return (
420✔
358
        isDeterministic(data[0], buildState.engine, buildState) &&
480✔
359
        isDeterministic(data[1], buildState.engine, {
360
          ...buildState,
361
          insideIterator: true
362
        })
363
      )
364
    },
365
    compile: (data, buildState) => {
366
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
336!
367
      const { async } = buildState
336✔
368
      let [selector, mapper, defaultValue] = data
336✔
369
      selector = buildString(selector, buildState)
336✔
370
      if (typeof defaultValue !== 'undefined') {
336!
371
        defaultValue = buildString(defaultValue, buildState)
336✔
372
      }
373
      const mapState = {
336✔
374
        ...buildState,
375
        extraArguments: 'above',
376
        avoidInlineAsync: true
377
      }
378
      mapper = build(mapper, mapState)
336✔
379
      const aboveArray = mapper.aboveDetected ? '[null, context, above]' : 'null'
336!
380

381
      buildState.methods.push(mapper)
336✔
382
      if (async) {
336✔
383
        if (!isSync(mapper) || selector.includes('await')) {
168!
384
          buildState.detectAsync = true
×
385
          if (typeof defaultValue !== 'undefined') {
×
386
            return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
387
              buildState.methods.length - 1
388
            }]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
389
          }
390
          return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
391
            buildState.methods.length - 1
392
          }]({ accumulator: a, current: b }, ${aboveArray}))`
393
        }
394
      }
395
      if (typeof defaultValue !== 'undefined') {
336!
396
        return `(${selector} || []).reduce((a,b) => methods[${
336✔
397
          buildState.methods.length - 1
398
        }]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
399
      }
400
      return `(${selector} || []).reduce((a,b) => methods[${
×
401
        buildState.methods.length - 1
402
      }]({ accumulator: a, current: b }, ${aboveArray}))`
403
    },
404
    method: (input, context, above, engine) => {
405
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
432✔
406
      let [selector, mapper, defaultValue] = input
408✔
407
      defaultValue = engine.run(defaultValue, context, {
408✔
408
        above
409
      })
410
      selector =
408✔
411
        engine.run(selector, context, {
456✔
412
          above
413
        }) || []
414
      const func = (accumulator, current) => {
408✔
415
        return engine.run(
1,410✔
416
          mapper,
417
          {
418
            accumulator,
419
            current
420
          },
421
          {
422
            above: [selector, context, above]
423
          }
424
        )
425
      }
426
      if (typeof defaultValue === 'undefined') {
408✔
427
        return selector.reduce(func)
54✔
428
      }
429
      return selector.reduce(func, defaultValue)
354✔
430
    },
431
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
408✔
432
    asyncMethod: async (input, context, above, engine) => {
433
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
24!
434
      let [selector, mapper, defaultValue] = input
24✔
435
      defaultValue = await engine.run(defaultValue, context, {
24✔
436
        above
437
      })
438
      selector =
24✔
439
        (await engine.run(selector, context, {
24!
440
          above
441
        })) || []
442
      return asyncIterators.reduce(
24✔
443
        selector,
444
        (accumulator, current) => {
445
          return engine.run(
102✔
446
            mapper,
447
            {
448
              accumulator,
449
              current
450
            },
451
            {
452
              above: [selector, context, above]
453
            }
454
          )
455
        },
456
        defaultValue
457
      )
458
    },
459
    traverse: false
460
  },
461
  '!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value),
816!
462
  '!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)),
324!
463
  cat: (arr) => {
464
    if (typeof arr === 'string') return arr
912!
465
    let res = ''
912✔
466
    for (let i = 0; i < arr.length; i++) res += arr[i]
2,244✔
467
    return res
912✔
468
  },
469
  keys: ([obj]) => typeof obj === 'object' ? Object.keys(obj) : [],
48✔
470
  pipe: {
471
    traverse: false,
472
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
168✔
473
    method: (args, context, above, engine) => {
474
      if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
108!
475
      let answer = engine.run(args[0], context, { above: [args, context, above] })
108✔
476
      for (let i = 1; i < args.length; i++) answer = engine.run(args[i], answer, { above: [args, context, above] })
108✔
477
      return answer
108✔
478
    },
479
    asyncMethod: async (args, context, above, engine) => {
480
      if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
60!
481
      let answer = await engine.run(args[0], context, { above: [args, context, above] })
60✔
482
      for (let i = 1; i < args.length; i++) answer = await engine.run(args[i], answer, { above: [args, context, above] })
96✔
483
      return answer
60✔
484
    },
485
    compile: (args, buildState) => {
486
      let res = buildState.compile`${args[0]}`
48✔
487
      for (let i = 1; i < args.length; i++) res = buildState.compile`${build(args[i], { ...buildState, extraArguments: 'above' })}(${res}, [null, context, above])`
48✔
488
      return res
48✔
489
    },
490
    deterministic: (data, buildState) => {
491
      if (!Array.isArray(data)) return false
192!
492
      data = [...data]
192✔
493
      const first = data.shift()
192✔
494
      return isDeterministic(first, buildState.engine, buildState) && isDeterministic(data, buildState.engine, { ...buildState, insideIterator: true })
192✔
495
    }
496
  },
497
  eachKey: {
498
    traverse: false,
499
    [Sync]: (data, buildState) => isSyncDeep(Object.values(data[Object.keys(data)[0]]), buildState.engine, buildState),
30✔
500
    method: (object, context, above, engine) => {
501
      const result = Object.keys(object).reduce((accumulator, key) => {
78✔
502
        const item = object[key]
138✔
503
        Object.defineProperty(accumulator, key, {
138✔
504
          enumerable: true,
505
          value: engine.run(item, context, { above })
506
        })
507
        return accumulator
138✔
508
      }, {})
509
      return result
78✔
510
    },
511
    deterministic: (data, buildState) => {
512
      if (data && typeof data === 'object') {
192!
513
        return Object.values(data).every((i) => {
192✔
514
          return isDeterministic(i, buildState.engine, buildState)
252✔
515
        })
516
      }
517
      throw new InvalidControlInput(data)
×
518
    },
519
    compile: (data, buildState) => {
520
      // what's nice about this is that I don't have to worry about whether it's async or not, the lower entries take care of that ;)
521
      // however, this is not engineered support yields, I will have to make a note of that & possibly support it at a later point.
522
      if (data && typeof data === 'object') {
168!
523
        const result = `({ ${Object.keys(data)
168✔
524
          .reduce((accumulator, key) => {
525
            accumulator.push(
240✔
526
              // @ts-ignore Never[] is not accurate
527
              `${JSON.stringify(key)}: ${buildString(data[key], buildState)}`
528
            )
529
            return accumulator
240✔
530
          }, [])
531
          .join(',')} })`
532
        return result
168✔
533
      }
UNCOV
534
      throw new InvalidControlInput(data)
×
535
    },
536
    asyncMethod: async (object, context, above, engine) => {
537
      const result = await asyncIterators.reduce(
18✔
538
        Object.keys(object),
539
        async (accumulator, key) => {
540
          const item = object[key]
30✔
541
          Object.defineProperty(accumulator, key, {
30✔
542
            enumerable: true,
543
            value: await engine.run(item, context, { above })
544
          })
545
          return accumulator
30✔
546
        },
547
        {}
548
      )
549
      return result
18✔
550
    }
551
  }
552
}
553

554
function createArrayIterativeMethod (name, useTruthy = false) {
162✔
555
  return {
270✔
556
    deterministic: (data, buildState) => {
557
      return (
2,832✔
558
        isDeterministic(data[0], buildState.engine, buildState) &&
3,714✔
559
        isDeterministic(data[1], buildState.engine, {
560
          ...buildState,
561
          insideIterator: true
562
        })
563
      )
564
    },
565
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
1,734✔
566
    method: (input, context, above, engine) => {
567
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
2,034✔
568
      let [selector, mapper] = input
2,010✔
569
      selector =
2,010✔
570
        engine.run(selector, context, {
2,058✔
571
          above
572
        }) || []
573

574
      return selector[name]((i, index) => {
2,010✔
575
        const result = engine.run(mapper, i, {
4,914✔
576
          above: [{ item: selector, index }, context, above]
577
        })
578
        return useTruthy ? engine.truthy(result) : result
4,914✔
579
      })
580
    },
581
    asyncMethod: async (input, context, above, engine) => {
582
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
786!
583
      let [selector, mapper] = input
786✔
584
      selector =
786✔
585
        (await engine.run(selector, context, {
786!
586
          above
587
        })) || []
588
      return asyncIterators[name](selector, (i, index) => {
786✔
589
        const result = engine.run(mapper, i, {
1,494✔
590
          above: [{ item: selector, index }, context, above]
591
        })
592
        return useTruthy ? engine.truthy(result) : result
1,494✔
593
      })
594
    },
595
    compile: (data, buildState) => {
596
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
1,818!
597
      const { async } = buildState
1,818✔
598
      const [selector, mapper] = data
1,818✔
599

600
      const mapState = {
1,818✔
601
        ...buildState,
602
        avoidInlineAsync: true,
603
        iteratorCompile: true,
604
        extraArguments: 'index, above'
605
      }
606

607
      const method = build(mapper, mapState)
1,818✔
608
      const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null, index: x }, context, above]` : buildState.compile`null`
1,818✔
609

610
      if (async) {
1,818✔
611
        if (!isSyncDeep(mapper, buildState.engine, buildState)) {
918✔
612
          buildState.detectAsync = true
126✔
613
          return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x) => ${method}(i, x, ${aboveArray}))`
126✔
614
        }
615
      }
616

617
      return buildState.compile`(${selector} || [])[${name}]((i, x) => ${method}(i, x, ${aboveArray}))`
1,692✔
618
    },
619
    traverse: false
620
  }
621
}
622
defaultMethods['?:'] = defaultMethods.if
54✔
623
// declare all of the functions here synchronous
624
Object.keys(defaultMethods).forEach((item) => {
54✔
625
  if (typeof defaultMethods[item] === 'function') {
2,322✔
626
    defaultMethods[item][Sync] = true
1,566✔
627
  }
628
  defaultMethods[item].deterministic =
2,322✔
629
    typeof defaultMethods[item].deterministic === 'undefined'
2,322✔
630
      ? true
631
      : defaultMethods[item].deterministic
632
})
633
// @ts-ignore Allow custom attribute
634
defaultMethods.var.deterministic = (data, buildState) => {
54✔
635
  return buildState.insideIterator && !String(data).includes('../../')
16,020✔
636
}
637
Object.assign(defaultMethods.missing, {
54✔
638
  deterministic: false
639
})
640
Object.assign(defaultMethods.missing_some, {
54✔
641
  deterministic: false
642
})
643
// @ts-ignore Allow custom attribute
644
defaultMethods['<'].compile = function (data, buildState) {
54✔
645
  if (!Array.isArray(data)) return false
600✔
646
  if (data.length === 2) return buildState.compile`(${data[0]} < ${data[1]})`
576✔
647
  if (data.length === 3) return buildState.compile`(${data[0]} < ${data[1]} && ${data[1]} < ${data[2]})`
72!
UNCOV
648
  return false
×
649
}
650
// @ts-ignore Allow custom attribute
651
defaultMethods['<='].compile = function (data, buildState) {
54✔
652
  if (!Array.isArray(data)) return false
168✔
653
  if (data.length === 2) return buildState.compile`(${data[0]} <= ${data[1]})`
144✔
654
  if (data.length === 3) return buildState.compile`(${data[0]} <= ${data[1]} && ${data[1]} <= ${data[2]})`
48!
UNCOV
655
  return false
×
656
}
657
// @ts-ignore Allow custom attribute
658
defaultMethods.min.compile = function (data, buildState) {
54✔
659
  if (!Array.isArray(data)) return false
120✔
660
  return `Math.min(${data
96✔
661
    .map((i) => buildString(i, buildState))
240✔
662
    .join(', ')})`
663
}
664
// @ts-ignore Allow custom attribute
665
defaultMethods.max.compile = function (data, buildState) {
54✔
666
  if (!Array.isArray(data)) return false
264✔
667
  return `Math.max(${data
144✔
668
    .map((i) => buildString(i, buildState))
336✔
669
    .join(', ')})`
670
}
671
// @ts-ignore Allow custom attribute
672
defaultMethods['>'].compile = function (data, buildState) {
54✔
673
  if (!Array.isArray(data)) return false
384✔
674
  if (data.length !== 2) return false
360!
675
  return buildState.compile`(${data[0]} > ${data[1]})`
360✔
676
}
677
// @ts-ignore Allow custom attribute
678
defaultMethods['>='].compile = function (data, buildState) {
54✔
679
  if (!Array.isArray(data)) return false
456✔
680
  if (data.length !== 2) return false
432!
681
  return buildState.compile`(${data[0]} >= ${data[1]})`
432✔
682
}
683
// @ts-ignore Allow custom attribute
684
defaultMethods['=='].compile = function (data, buildState) {
54✔
685
  if (!Array.isArray(data)) return false
240✔
686
  if (data.length !== 2) return false
216!
687
  return buildState.compile`(${data[0]} == ${data[1]})`
216✔
688
}
689
// @ts-ignore Allow custom attribute
690
defaultMethods['!='].compile = function (data, buildState) {
54✔
691
  if (!Array.isArray(data)) return false
96✔
692
  if (data.length !== 2) return false
72!
693
  return buildState.compile`(${data[0]} != ${data[1]})`
72✔
694
}
695
// @ts-ignore Allow custom attribute
696
defaultMethods.if.compile = function (data, buildState) {
54✔
697
  if (!Array.isArray(data)) return false
1,416!
698
  if (data.length < 3) return false
1,416✔
699

700
  data = [...data]
1,272✔
701
  if (data.length % 2 !== 1) data.push(null)
1,272✔
702
  const onFalse = data.pop()
1,272✔
703

704
  let res = buildState.compile``
1,272✔
705
  while (data.length) {
1,272✔
706
    const condition = data.shift()
1,896✔
707
    const onTrue = data.shift()
1,896✔
708
    res = buildState.compile`${res} engine.truthy(${condition}) ? ${onTrue} : `
1,896✔
709
  }
710

711
  return buildState.compile`(${res} ${onFalse})`
1,272✔
712
}
713
// @ts-ignore Allow custom attribute
714
defaultMethods['==='].compile = function (data, buildState) {
54✔
715
  if (!Array.isArray(data)) return false
240✔
716
  if (data.length !== 2) return false
216!
717
  return buildState.compile`(${data[0]} === ${data[1]})`
216✔
718
}
719
// @ts-ignore Allow custom attribute
720
defaultMethods['+'].compile = function (data, buildState) {
54✔
721
  if (Array.isArray(data)) {
954✔
722
    return `(${data
930✔
723
      .map((i) => `(+${buildString(i, buildState)})`)
1,914✔
724
      .join(' + ')})`
725
  } else if (typeof data === 'string' || typeof data === 'number') {
24!
UNCOV
726
    return `(+${buildString(data, buildState)})`
×
727
  } else {
728
    return `([].concat(${buildString(
24✔
729
      data,
730
      buildState
731
    )})).reduce((a,b) => (+a)+(+b), 0)`
732
  }
733
}
734

735
// @ts-ignore Allow custom attribute
736
defaultMethods['%'].compile = function (data, buildState) {
54✔
737
  if (Array.isArray(data)) {
168✔
738
    return `(${data
144✔
739
      .map((i) => `(+${buildString(i, buildState)})`)
288✔
740
      .join(' % ')})`
741
  } else {
742
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+b))`
24✔
743
  }
744
}
745

746
// @ts-ignore Allow custom attribute
747
defaultMethods.or.compile = function (data, buildState) {
54✔
748
  if (!buildState.engine.truthy.IDENTITY) return false
384✔
749
  if (Array.isArray(data)) {
24!
750
    return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
×
751
  } else {
752
    return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
24✔
753
  }
754
}
755

756
// @ts-ignore Allow custom attribute
757
defaultMethods.in.compile = function (data, buildState) {
54✔
758
  if (!Array.isArray(data)) return false
216!
759
  return buildState.compile`(${data[1]} || []).includes(${data[0]})`
216✔
760
}
761

762
// @ts-ignore Allow custom attribute
763
defaultMethods.and.compile = function (data, buildState) {
54✔
764
  if (!buildState.engine.truthy.IDENTITY) return false
528✔
765
  if (Array.isArray(data)) {
24!
UNCOV
766
    return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
×
767
  } else {
768
    return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
24✔
769
  }
770
}
771

772
// @ts-ignore Allow custom attribute
773
defaultMethods['-'].compile = function (data, buildState) {
54✔
774
  if (Array.isArray(data)) {
216✔
775
    return `${data.length === 1 ? '-' : ''}(${data
192✔
776
      .map((i) => `(+${buildString(i, buildState)})`)
264✔
777
      .join(' - ')})`
778
  }
779
  if (typeof data === 'string' || typeof data === 'number') {
24!
UNCOV
780
    return `(-${buildString(data, buildState)})`
×
781
  } else {
782
    return `((a=>(a.length===1?a[0]=-a[0]:a)&0||a)([].concat(${buildString(
24✔
783
      data,
784
      buildState
785
    )}))).reduce((a,b) => (+a)-(+b))`
786
  }
787
}
788
// @ts-ignore Allow custom attribute
789
defaultMethods['/'].compile = function (data, buildState) {
54✔
790
  if (Array.isArray(data)) {
120✔
791
    return `(${data
96✔
792
      .map((i) => `(+${buildString(i, buildState)})`)
192✔
793
      .join(' / ')})`
794
  } else {
795
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))`
24✔
796
  }
797
}
798
// @ts-ignore Allow custom attribute
799
defaultMethods['*'].compile = function (data, buildState) {
54✔
800
  if (Array.isArray(data)) {
336✔
801
    return `(${data
312✔
802
      .map((i) => `(+${buildString(i, buildState)})`)
624✔
803
      .join(' * ')})`
804
  } else {
805
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))`
24✔
806
  }
807
}
808
// @ts-ignore Allow custom attribute
809
defaultMethods.cat.compile = function (data, buildState) {
54✔
810
  if (typeof data === 'string') return JSON.stringify(data)
384!
811
  if (!Array.isArray(data)) return false
384✔
812
  let res = buildState.compile`''`
288✔
813
  for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}`
648✔
814
  return buildState.compile`(${res})`
288✔
815
}
816

817
// @ts-ignore Allow custom attribute
818
defaultMethods['!'].compile = function (
54✔
819
  data,
820
  buildState
821
) {
822
  if (Array.isArray(data)) return buildState.compile`(!engine.truthy(${data[0]}))`
264!
UNCOV
823
  return buildState.compile`(!engine.truthy(${data}))`
×
824
}
825

826
defaultMethods.not = defaultMethods['!']
54✔
827

828
// @ts-ignore Allow custom attribute
829
defaultMethods['!!'].compile = function (data, buildState) {
54✔
830
  if (Array.isArray(data)) return buildState.compile`(!!engine.truthy(${data[0]}))`
96!
UNCOV
831
  return `(!!engine.truthy(${data}))`
×
832
}
833
defaultMethods.none.deterministic = defaultMethods.some.deterministic
54✔
834
defaultMethods.get.compile = function (data, buildState) {
54✔
835
  let defaultValue = null
396✔
836
  let key = data
396✔
837
  let obj = null
396✔
838
  if (Array.isArray(data) && data.length <= 3) {
396!
839
    obj = data[0]
396✔
840
    key = data[1]
396✔
841
    defaultValue = typeof data[2] === 'undefined' ? null : data[2]
396✔
842

843
    // Bail out if the key is dynamic; dynamic keys are not really optimized by this block.
844
    if (key && typeof key === 'object') return false
396✔
845

846
    key = key.toString()
288✔
847
    const pieces = splitPathMemoized(key)
288✔
848
    if (!chainingSupported) {
288✔
849
      return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
48✔
850
        (text, i) => {
851
          return `(${text}||0)[${JSON.stringify(i)}]`
48✔
852
        },
853
        `(${buildString(obj, buildState)}||0)`
854
      )}, ${buildString(defaultValue, buildState)}))`
855
    }
856
    return `((${buildString(obj, buildState)})${pieces
240✔
857
      .map((i) => `?.[${buildString(i, buildState)}]`)
240✔
858
      .join('')} ?? ${buildString(defaultValue, buildState)})`
859
  }
UNCOV
860
  return false
×
861
}
862
// @ts-ignore Allow custom attribute
863
defaultMethods.var.compile = function (data, buildState) {
54✔
864
  let key = data
7,638✔
865
  let defaultValue = null
7,638✔
866
  buildState.varTop = buildState.varTop || new Set()
7,638✔
867
  if (
7,638!
868
    !key ||
38,190✔
869
    typeof data === 'string' ||
870
    typeof data === 'number' ||
871
    (Array.isArray(data) && data.length <= 2)
872
  ) {
873
    if (Array.isArray(data)) {
7,638!
874
      key = data[0]
7,638✔
875
      defaultValue = typeof data[1] === 'undefined' ? null : data[1]
7,638✔
876
    }
877

878
    if (key === '../index' && buildState.iteratorCompile) return 'index'
7,638✔
879

880
    // this counts the number of var accesses to determine if they're all just using this override.
881
    // this allows for a small optimization :)
882
    if (typeof key === 'undefined' || key === null || key === '') return 'context'
7,590✔
883
    if (typeof key !== 'string' && typeof key !== 'number') return false
6,612✔
884

885
    key = key.toString()
6,564✔
886
    if (key.includes('../')) return false
6,564✔
887

888
    const pieces = splitPathMemoized(key)
6,222✔
889
    const [top] = pieces
6,222✔
890
    buildState.varTop.add(top)
6,222✔
891

892
    if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a
8,916✔
893
    else buildState.methods.preventFunctions = a => a
60✔
894

895
    // support older versions of node
896
    if (!chainingSupported) {
6,222✔
897
      return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
1,037✔
898
        (text, i) => `(${text}||0)[${JSON.stringify(i)}]`,
1,213✔
899
        '(context||0)'
900
      )}, ${buildString(defaultValue, buildState)})))`
901
    }
902
    return `(methods.preventFunctions(context${pieces
5,185✔
903
      .map((i) => `?.[${JSON.stringify(i)}]`)
6,065✔
904
      .join('')} ?? ${buildString(defaultValue, buildState)}))`
905
  }
UNCOV
906
  return false
×
907
}
908

909
// @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations
910
defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods.var.optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true
54✔
911

912
export default {
913
  ...defaultMethods
914
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc