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

TotalTechGeek / json-logic-engine / 12171983738

05 Dec 2024 02:20AM UTC coverage: 92.178% (-1.2%) from 93.354%
12171983738

push

github

web-flow
Merge pull request #37 from TotalTechGeek/fix/strictly-honor-sugaring

This set of changes allows JSON Logic engine to more strictly honor sugaring

760 of 860 branches covered (88.37%)

Branch coverage included in aggregate %.

33 of 33 new or added lines in 8 files covered. (100.0%)

6 existing lines in 1 file now uncovered.

772 of 802 relevant lines covered (96.26%)

32398.77 hits per line

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

90.07
/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)) {
23,040✔
14
    return method.every((i) => isDeterministic(i, engine, buildState))
14,832✔
15
  }
16
  if (method && typeof method === 'object') {
18,024✔
17
    const func = Object.keys(method)[0]
4,878✔
18
    const lower = method[func]
4,878✔
19

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

23
    if (engine.methods[func].traverse === false) {
4,794✔
24
      return typeof engine.methods[func].deterministic === 'function'
168!
25
        ? engine.methods[func].deterministic(lower, buildState)
26
        : engine.methods[func].deterministic
27
    }
28
    return typeof engine.methods[func].deterministic === 'function'
4,626✔
29
      ? engine.methods[func].deterministic(lower, buildState)
30
      : engine.methods[func].deterministic &&
2,490✔
31
          isDeterministic(lower, engine, buildState)
32
  }
33
  return true
13,146✔
34
}
35

36
function isSyncDeep (method, engine, buildState) {
37
  if (Array.isArray(method)) {
22,764✔
38
    return method.every((i) => isSyncDeep(i, engine, buildState))
12,720✔
39
  }
40

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

50
  return true
11,220✔
51
}
52

53
const defaultMethods = {
54✔
54
  '+': (data) => {
55
    if (typeof data === 'string') return +data
482,100✔
56
    if (typeof data === 'number') return +data
482,088!
57
    let res = 0
482,088✔
58
    for (let i = 0; i < data.length; i++) res += +data[i]
964,164✔
59
    return res
482,088✔
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,186!
97

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

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

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

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

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

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

118
      return engine.run(onFalse, context, { above })
942✔
119
    },
120
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
3,564✔
121
    deterministic: (data, buildState) => {
122
      return isDeterministic(data, buildState.engine, buildState)
3,168✔
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,128✔
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
  var: (key, context, above, engine) => {
207
    let b
208
    if (Array.isArray(key)) {
18,186✔
209
      b = key[1]
13,128✔
210
      key = key[0]
13,128✔
211
    }
212
    let iter = 0
18,186✔
213
    while (
18,186✔
214
      typeof key === 'string' &&
43,248✔
215
      key.startsWith('../') &&
216
      iter < above.length
217
    ) {
218
      context = above[iter++]
2,340✔
219
      key = key.substring(3)
2,340✔
220
      // A performance optimization that allows you to pass the previous above array without spreading it as the last argument
221
      if (iter === above.length && Array.isArray(context)) {
2,340✔
222
        iter = 0
288✔
223
        above = context
288✔
224
        context = above[iter++]
288✔
225
      }
226
    }
227

228
    const notFound = b === undefined ? null : b
18,186✔
229
    if (typeof key === 'undefined' || key === '' || key === null) {
18,186✔
230
      if (engine.allowFunctions || typeof context !== 'function') {
2,808!
231
        return context
2,808✔
232
      }
233
      return null
×
234
    }
235
    const subProps = splitPathMemoized(String(key))
15,378✔
236
    for (let i = 0; i < subProps.length; i++) {
15,378✔
237
      if (context === null || context === undefined) {
16,746✔
238
        return notFound
864✔
239
      }
240
      // Descending into context
241
      context = context[subProps[i]]
15,882✔
242
      if (context === undefined) {
15,882✔
243
        return notFound
2,376✔
244
      }
245
    }
246
    if (engine.allowFunctions || typeof context !== 'function') {
12,138✔
247
      return context
12,066✔
248
    }
249
    return null
72✔
250
  },
251
  missing: (checked, context, above, engine) => {
252
    return (Array.isArray(checked) ? checked : [checked]).filter((key) => {
2,736!
253
      return defaultMethods.var(key, context, above, engine) === null
4,656✔
254
    })
255
  },
256
  missing_some: ([needCount, options], context, above, engine) => {
257
    const missing = defaultMethods.missing(options, context, above, engine)
864✔
258
    if (options.length - missing.length >= needCount) {
864✔
259
      return []
576✔
260
    } else {
261
      return missing
288✔
262
    }
263
  },
264
  map: createArrayIterativeMethod('map'),
265
  some: createArrayIterativeMethod('some', true),
266
  all: createArrayIterativeMethod('every', true),
267
  none: {
268
    traverse: false,
269
    // todo: add async build & build
270
    method: (val, context, above, engine) => {
271
      return !defaultMethods.some.method(val, context, above, engine)
192✔
272
    },
273
    asyncMethod: async (val, context, above, engine) => {
274
      return !(await defaultMethods.some.asyncMethod(
192✔
275
        val,
276
        context,
277
        above,
278
        engine
279
      ))
280
    },
281
    compile: (data, buildState) => {
282
      const result = defaultMethods.some.compile(data, buildState)
384✔
283
      return result ? buildState.compile`!(${result})` : false
384!
284
    }
285
  },
286
  merge: (arrays) => (Array.isArray(arrays) ? [].concat(...arrays) : [arrays]),
1,140!
287
  every: createArrayIterativeMethod('every'),
288
  filter: createArrayIterativeMethod('filter'),
289
  reduce: {
290
    deterministic: (data, buildState) => {
291
      return (
372✔
292
        isDeterministic(data[0], buildState.engine, buildState) &&
432✔
293
        isDeterministic(data[1], buildState.engine, {
294
          ...buildState,
295
          insideIterator: true
296
        })
297
      )
298
    },
299
    compile: (data, buildState) => {
300
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
288!
301
      const { async } = buildState
288✔
302
      let [selector, mapper, defaultValue] = data
288✔
303
      selector = buildString(selector, buildState)
288✔
304
      if (typeof defaultValue !== 'undefined') {
288!
305
        defaultValue = buildString(defaultValue, buildState)
288✔
306
      }
307
      const mapState = {
288✔
308
        ...buildState,
309
        extraArguments: 'above',
310
        avoidInlineAsync: true
311
      }
312
      mapper = build(mapper, mapState)
288✔
313
      buildState.methods.push(mapper)
288✔
314
      if (async) {
288✔
315
        if (!isSync(mapper) || selector.includes('await')) {
144!
316
          buildState.detectAsync = true
×
317
          if (typeof defaultValue !== 'undefined') {
×
318
            return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
319
              buildState.methods.length - 1
320
            }]({ accumulator: a, current: b }, [null, context, above]), ${defaultValue})`
321
          }
322
          return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
323
            buildState.methods.length - 1
324
          }]({ accumulator: a, current: b }, [null, context, above]))`
325
        }
326
      }
327
      if (typeof defaultValue !== 'undefined') {
288!
328
        return `(${selector} || []).reduce((a,b) => methods[${
288✔
329
          buildState.methods.length - 1
330
        }]({ accumulator: a, current: b }, [null, context, above]), ${defaultValue})`
331
      }
332
      return `(${selector} || []).reduce((a,b) => methods[${
×
333
        buildState.methods.length - 1
334
      }]({ accumulator: a, current: b }, [null, context, above]))`
335
    },
336
    method: (input, context, above, engine) => {
337
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
384✔
338
      let [selector, mapper, defaultValue] = input
360✔
339
      defaultValue = engine.run(defaultValue, context, {
360✔
340
        above
341
      })
342
      selector =
360✔
343
        engine.run(selector, context, {
408✔
344
          above
345
        }) || []
346
      const func = (accumulator, current) => {
360✔
347
        return engine.run(
1,218✔
348
          mapper,
349
          {
350
            accumulator,
351
            current
352
          },
353
          {
354
            above: [selector, context, above]
355
          }
356
        )
357
      }
358
      if (typeof defaultValue === 'undefined') {
360✔
359
        return selector.reduce(func)
54✔
360
      }
361
      return selector.reduce(func, defaultValue)
306✔
362
    },
363
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
360✔
364
    asyncMethod: async (input, context, above, engine) => {
365
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
24!
366
      let [selector, mapper, defaultValue] = input
24✔
367
      defaultValue = await engine.run(defaultValue, context, {
24✔
368
        above
369
      })
370
      selector =
24✔
371
        (await engine.run(selector, context, {
24!
372
          above
373
        })) || []
374
      return asyncIterators.reduce(
24✔
375
        selector,
376
        (accumulator, current) => {
377
          return engine.run(
102✔
378
            mapper,
379
            {
380
              accumulator,
381
              current
382
            },
383
            {
384
              above: [selector, context, above]
385
            }
386
          )
387
        },
388
        defaultValue
389
      )
390
    },
391
    traverse: false
392
  },
393
  '!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value),
876!
394
  '!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)),
396!
395
  cat: (arr) => {
396
    if (typeof arr === 'string') return arr
912!
397
    let res = ''
912✔
398
    for (let i = 0; i < arr.length; i++) res += arr[i]
2,244✔
399
    return res
912✔
400
  },
401
  keys: ([obj]) => typeof obj === 'object' ? Object.keys(obj) : [],
48✔
402
  pipe: {
403
    traverse: false,
404
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
168✔
405
    method: (args, context, above, engine) => {
406
      if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
108!
407
      let answer = engine.run(args[0], context, { above: [args, context, above] })
108✔
408
      for (let i = 1; i < args.length; i++) answer = engine.run(args[i], answer, { above: [args, context, above] })
108✔
409
      return answer
108✔
410
    },
411
    asyncMethod: async (args, context, above, engine) => {
412
      if (!Array.isArray(args)) throw new Error('Data for pipe must be an array')
60!
413
      let answer = await engine.run(args[0], context, { above: [args, context, above] })
60✔
414
      for (let i = 1; i < args.length; i++) answer = await engine.run(args[i], answer, { above: [args, context, above] })
96✔
415
      return answer
60✔
416
    },
417
    compile: (args, buildState) => {
418
      let res = buildState.compile`${args[0]}`
48✔
419
      for (let i = 1; i < args.length; i++) res = buildState.compile`${build(args[i], { ...buildState, extraArguments: 'above' })}(${res}, [null, context, above])`
48✔
420
      return res
48✔
421
    },
422
    deterministic: (data, buildState) => {
423
      if (!Array.isArray(data)) return false
192!
424
      data = [...data]
192✔
425
      const first = data.shift()
192✔
426
      return isDeterministic(first, buildState.engine, buildState) && isDeterministic(data, buildState.engine, { ...buildState, insideIterator: true })
192✔
427
    }
428
  },
429
  eachKey: {
430
    traverse: false,
431
    [Sync]: (data, buildState) => isSyncDeep(Object.values(data[Object.keys(data)[0]]), buildState.engine, buildState),
30✔
432
    method: (object, context, above, engine) => {
433
      const result = Object.keys(object).reduce((accumulator, key) => {
78✔
434
        const item = object[key]
138✔
435
        Object.defineProperty(accumulator, key, {
138✔
436
          enumerable: true,
437
          value: engine.run(item, context, { above })
438
        })
439
        return accumulator
138✔
440
      }, {})
441
      return result
78✔
442
    },
443
    deterministic: (data, buildState) => {
444
      if (data && typeof data === 'object') {
192!
445
        return Object.values(data).every((i) => {
192✔
446
          return isDeterministic(i, buildState.engine, buildState)
252✔
447
        })
448
      }
UNCOV
449
      throw new InvalidControlInput(data)
×
450
    },
451
    compile: (data, buildState) => {
452
      // 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 ;)
453
      // however, this is not engineered support yields, I will have to make a note of that & possibly support it at a later point.
454
      if (data && typeof data === 'object') {
168!
455
        const result = `({ ${Object.keys(data)
168✔
456
          .reduce((accumulator, key) => {
457
            accumulator.push(
240✔
458
              // @ts-ignore Never[] is not accurate
459
              `${JSON.stringify(key)}: ${buildString(data[key], buildState)}`
460
            )
461
            return accumulator
240✔
462
          }, [])
463
          .join(',')} })`
464
        return result
168✔
465
      }
UNCOV
466
      throw new InvalidControlInput(data)
×
467
    },
468
    asyncMethod: async (object, context, above, engine) => {
469
      const result = await asyncIterators.reduce(
18✔
470
        Object.keys(object),
471
        async (accumulator, key) => {
472
          const item = object[key]
30✔
473
          Object.defineProperty(accumulator, key, {
30✔
474
            enumerable: true,
475
            value: await engine.run(item, context, { above })
476
          })
477
          return accumulator
30✔
478
        },
479
        {}
480
      )
481
      return result
18✔
482
    }
483
  }
484
}
485

486
function createArrayIterativeMethod (name, useTruthy = false) {
162✔
487
  return {
270✔
488
    deterministic: (data, buildState) => {
489
      return (
2,304✔
490
        isDeterministic(data[0], buildState.engine, buildState) &&
2,850✔
491
        isDeterministic(data[1], buildState.engine, {
492
          ...buildState,
493
          insideIterator: true
494
        })
495
      )
496
    },
497
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
1,134✔
498
    method: (input, context, above, engine) => {
499
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
1,698✔
500
      let [selector, mapper] = input
1,674✔
501
      selector =
1,674✔
502
        engine.run(selector, context, {
1,722✔
503
          above
504
        }) || []
505

506
      return selector[name]((i, index) => {
1,674✔
507
        const result = engine.run(mapper, i, {
3,618✔
508
          above: [{ item: selector, index }, context, above]
509
        })
510
        return useTruthy ? engine.truthy(result) : result
3,618✔
511
      })
512
    },
513
    asyncMethod: async (input, context, above, engine) => {
514
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
642!
515
      let [selector, mapper] = input
642✔
516
      selector =
642✔
517
        (await engine.run(selector, context, {
642!
518
          above
519
        })) || []
520
      return asyncIterators[name](selector, (i, index) => {
642✔
521
        const result = engine.run(mapper, i, {
1,110✔
522
          above: [{ item: selector, index }, context, above]
523
        })
524
        return useTruthy ? engine.truthy(result) : result
1,110✔
525
      })
526
    },
527
    compile: (data, buildState) => {
528
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
1,410!
529
      const { async } = buildState
1,410✔
530
      const [selector, mapper] = data
1,410✔
531

532
      const mapState = {
1,410✔
533
        ...buildState,
534
        avoidInlineAsync: true,
535
        iteratorCompile: true,
536
        extraArguments: 'index, above'
537
      }
538

539
      if (async) {
1,410✔
540
        if (!isSyncDeep(mapper, buildState.engine, buildState)) {
714✔
541
          buildState.detectAsync = true
18✔
542
          return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, above]))`
18✔
543
        }
544
      }
545

546
      return buildState.compile`(${selector} || [])[${name}]((i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, above]))`
1,392✔
547
    },
548
    traverse: false
549
  }
550
}
551
defaultMethods['?:'] = defaultMethods.if
54✔
552
// declare all of the functions here synchronous
553
Object.keys(defaultMethods).forEach((item) => {
54✔
554
  if (typeof defaultMethods[item] === 'function') {
2,268✔
555
    defaultMethods[item][Sync] = true
1,566✔
556
  }
557
  defaultMethods[item].deterministic =
2,268✔
558
    typeof defaultMethods[item].deterministic === 'undefined'
2,268✔
559
      ? true
560
      : defaultMethods[item].deterministic
561
})
562
// @ts-ignore Allow custom attribute
563
defaultMethods.var.deterministic = (data, buildState) => {
54✔
564
  return buildState.insideIterator && !String(data).includes('../../')
13,680✔
565
}
566
Object.assign(defaultMethods.missing, {
54✔
567
  deterministic: false
568
})
569
Object.assign(defaultMethods.missing_some, {
54✔
570
  deterministic: false
571
})
572
// @ts-ignore Allow custom attribute
573
defaultMethods['<'].compile = function (data, buildState) {
54✔
574
  if (!Array.isArray(data)) return false
600✔
575
  if (data.length === 2) return buildState.compile`(${data[0]} < ${data[1]})`
576✔
576
  if (data.length === 3) return buildState.compile`(${data[0]} < ${data[1]} && ${data[1]} < ${data[2]})`
72!
577
  return false
×
578
}
579
// @ts-ignore Allow custom attribute
580
defaultMethods['<='].compile = function (data, buildState) {
54✔
581
  if (!Array.isArray(data)) return false
168✔
582
  if (data.length === 2) return buildState.compile`(${data[0]} <= ${data[1]})`
144✔
583
  if (data.length === 3) return buildState.compile`(${data[0]} <= ${data[1]} && ${data[1]} <= ${data[2]})`
48!
584
  return false
×
585
}
586
// @ts-ignore Allow custom attribute
587
defaultMethods.min.compile = function (data, buildState) {
54✔
588
  if (!Array.isArray(data)) return false
120✔
589
  return `Math.min(${data
96✔
590
    .map((i) => buildString(i, buildState))
240✔
591
    .join(', ')})`
592
}
593
// @ts-ignore Allow custom attribute
594
defaultMethods.max.compile = function (data, buildState) {
54✔
595
  if (!Array.isArray(data)) return false
264✔
596
  return `Math.max(${data
144✔
597
    .map((i) => buildString(i, buildState))
336✔
598
    .join(', ')})`
599
}
600
// @ts-ignore Allow custom attribute
601
defaultMethods['>'].compile = function (data, buildState) {
54✔
602
  if (!Array.isArray(data)) return false
384✔
603
  if (data.length !== 2) return false
360!
604
  return buildState.compile`(${data[0]} > ${data[1]})`
360✔
605
}
606
// @ts-ignore Allow custom attribute
607
defaultMethods['>='].compile = function (data, buildState) {
54✔
608
  if (!Array.isArray(data)) return false
456✔
609
  if (data.length !== 2) return false
432!
610
  return buildState.compile`(${data[0]} >= ${data[1]})`
432✔
611
}
612
// @ts-ignore Allow custom attribute
613
defaultMethods['=='].compile = function (data, buildState) {
54✔
614
  if (!Array.isArray(data)) return false
240✔
615
  if (data.length !== 2) return false
216!
616
  return buildState.compile`(${data[0]} == ${data[1]})`
216✔
617
}
618
// @ts-ignore Allow custom attribute
619
defaultMethods['!='].compile = function (data, buildState) {
54✔
620
  if (!Array.isArray(data)) return false
96✔
621
  if (data.length !== 2) return false
72!
622
  return buildState.compile`(${data[0]} != ${data[1]})`
72✔
623
}
624
// @ts-ignore Allow custom attribute
625
defaultMethods.if.compile = function (data, buildState) {
54✔
626
  if (!Array.isArray(data)) return false
1,368!
627
  if (data.length < 3) return false
1,368✔
628

629
  data = [...data]
1,224✔
630
  if (data.length % 2 !== 1) data.push(null)
1,224✔
631
  const onFalse = data.pop()
1,224✔
632

633
  let res = buildState.compile``
1,224✔
634
  while (data.length) {
1,224✔
635
    const condition = data.shift()
1,848✔
636
    const onTrue = data.shift()
1,848✔
637
    res = buildState.compile`${res} engine.truthy(${condition}) ? ${onTrue} : `
1,848✔
638
  }
639

640
  return buildState.compile`(${res} ${onFalse})`
1,224✔
641
}
642
// @ts-ignore Allow custom attribute
643
defaultMethods['==='].compile = function (data, buildState) {
54✔
644
  if (!Array.isArray(data)) return false
192✔
645
  if (data.length !== 2) return false
168!
646
  return buildState.compile`(${data[0]} === ${data[1]})`
168✔
647
}
648
// @ts-ignore Allow custom attribute
649
defaultMethods['+'].compile = function (data, buildState) {
54✔
650
  if (Array.isArray(data)) {
594✔
651
    return `(${data
570✔
652
      .map((i) => `(+${buildString(i, buildState)})`)
1,194✔
653
      .join(' + ')})`
654
  } else if (typeof data === 'string' || typeof data === 'number') {
24!
UNCOV
655
    return `(+${buildString(data, buildState)})`
×
656
  } else {
657
    return `([].concat(${buildString(
24✔
658
      data,
659
      buildState
660
    )})).reduce((a,b) => (+a)+(+b), 0)`
661
  }
662
}
663

664
// @ts-ignore Allow custom attribute
665
defaultMethods['%'].compile = function (data, buildState) {
54✔
666
  if (Array.isArray(data)) {
168✔
667
    return `(${data
144✔
668
      .map((i) => `(+${buildString(i, buildState)})`)
288✔
669
      .join(' % ')})`
670
  } else {
671
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+b))`
24✔
672
  }
673
}
674

675
// @ts-ignore Allow custom attribute
676
defaultMethods.or.compile = function (data, buildState) {
54✔
677
  if (!buildState.engine.truthy.IDENTITY) return false
384✔
678
  if (Array.isArray(data)) {
24!
679
    return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
×
680
  } else {
681
    return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
24✔
682
  }
683
}
684

685
// @ts-ignore Allow custom attribute
686
defaultMethods.in.compile = function (data, buildState) {
54✔
687
  if (!Array.isArray(data)) return false
216!
688
  return buildState.compile`(${data[1]} || []).includes(${data[0]})`
216✔
689
}
690

691
// @ts-ignore Allow custom attribute
692
defaultMethods.and.compile = function (data, buildState) {
54✔
693
  if (!buildState.engine.truthy.IDENTITY) return false
528✔
694
  if (Array.isArray(data)) {
24!
695
    return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
×
696
  } else {
697
    return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
24✔
698
  }
699
}
700

701
// @ts-ignore Allow custom attribute
702
defaultMethods['-'].compile = function (data, buildState) {
54✔
703
  if (Array.isArray(data)) {
216✔
704
    return `${data.length === 1 ? '-' : ''}(${data
192✔
705
      .map((i) => `(+${buildString(i, buildState)})`)
264✔
706
      .join(' - ')})`
707
  }
708
  if (typeof data === 'string' || typeof data === 'number') {
24!
UNCOV
709
    return `(-${buildString(data, buildState)})`
×
710
  } else {
711
    return `((a=>(a.length===1?a[0]=-a[0]:a)&0||a)([].concat(${buildString(
24✔
712
      data,
713
      buildState
714
    )}))).reduce((a,b) => (+a)-(+b))`
715
  }
716
}
717
// @ts-ignore Allow custom attribute
718
defaultMethods['/'].compile = function (data, buildState) {
54✔
719
  if (Array.isArray(data)) {
120✔
720
    return `(${data
96✔
721
      .map((i) => `(+${buildString(i, buildState)})`)
192✔
722
      .join(' / ')})`
723
  } else {
724
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))`
24✔
725
  }
726
}
727
// @ts-ignore Allow custom attribute
728
defaultMethods['*'].compile = function (data, buildState) {
54✔
729
  if (Array.isArray(data)) {
336✔
730
    return `(${data
312✔
731
      .map((i) => `(+${buildString(i, buildState)})`)
624✔
732
      .join(' * ')})`
733
  } else {
734
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))`
24✔
735
  }
736
}
737
// @ts-ignore Allow custom attribute
738
defaultMethods.cat.compile = function (data, buildState) {
54✔
739
  if (typeof data === 'string') return JSON.stringify(data)
384!
740
  if (!Array.isArray(data)) return false
384✔
741
  let res = buildState.compile`''`
288✔
742
  for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}`
648✔
743
  return buildState.compile`(${res})`
288✔
744
}
745

746
// @ts-ignore Allow custom attribute
747
defaultMethods['!'].compile = function (
54✔
748
  data,
749
  buildState
750
) {
751
  if (Array.isArray(data)) return buildState.compile`(!engine.truthy(${data[0]}))`
288!
UNCOV
752
  return buildState.compile`(!engine.truthy(${data}))`
×
753
}
754

755
defaultMethods.not = defaultMethods['!']
54✔
756

757
// @ts-ignore Allow custom attribute
758
defaultMethods['!!'].compile = function (data, buildState) {
54✔
759
  if (Array.isArray(data)) return buildState.compile`(!!engine.truthy(${data[0]}))`
120!
UNCOV
760
  return `(!!engine.truthy(${data}))`
×
761
}
762
defaultMethods.none.deterministic = defaultMethods.some.deterministic
54✔
763
defaultMethods.get.compile = function (data, buildState) {
54✔
764
  let defaultValue = null
396✔
765
  let key = data
396✔
766
  let obj = null
396✔
767
  if (Array.isArray(data) && data.length <= 3) {
396!
768
    obj = data[0]
396✔
769
    key = data[1]
396✔
770
    defaultValue = typeof data[2] === 'undefined' ? null : data[2]
396✔
771

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

775
    key = key.toString()
288✔
776
    const pieces = splitPathMemoized(key)
288✔
777
    if (!chainingSupported) {
288✔
778
      return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
48✔
779
        (text, i) => {
780
          return `(${text}||0)[${JSON.stringify(i)}]`
48✔
781
        },
782
        `(${buildString(obj, buildState)}||0)`
783
      )}, ${buildString(defaultValue, buildState)}))`
784
    }
785
    return `((${buildString(obj, buildState)})${pieces
240✔
786
      .map((i) => `?.[${buildString(i, buildState)}]`)
240✔
787
      .join('')} ?? ${buildString(defaultValue, buildState)})`
788
  }
789
  return false
×
790
}
791
// @ts-ignore Allow custom attribute
792
defaultMethods.var.compile = function (data, buildState) {
54✔
793
  let key = data
6,102✔
794
  let defaultValue = null
6,102✔
795
  buildState.varTop = buildState.varTop || new Set()
6,102✔
796
  if (
6,102!
797
    !key ||
30,510✔
798
    typeof data === 'string' ||
799
    typeof data === 'number' ||
800
    (Array.isArray(data) && data.length <= 2)
801
  ) {
802
    if (Array.isArray(data)) {
6,102!
803
      key = data[0]
6,102✔
804
      defaultValue = typeof data[1] === 'undefined' ? null : data[1]
6,102✔
805
    }
806

807
    if (key === '../index' && buildState.iteratorCompile) return 'index'
6,102!
808

809
    // this counts the number of var accesses to determine if they're all just using this override.
810
    // this allows for a small optimization :)
811
    if (typeof key === 'undefined' || key === null || key === '') return 'context'
6,102✔
812
    if (typeof key !== 'string' && typeof key !== 'number') return false
5,220✔
813

814
    key = key.toString()
5,172✔
815
    if (key.includes('../')) return false
5,172✔
816

817
    const pieces = splitPathMemoized(key)
5,022✔
818
    const [top] = pieces
5,022✔
819
    buildState.varTop.add(top)
5,022✔
820

821
    if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a
6,996✔
822
    else buildState.methods.preventFunctions = a => a
60✔
823

824
    // support older versions of node
825
    if (!chainingSupported) {
5,022✔
826
      return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
837✔
827
        (text, i) => `(${text}||0)[${JSON.stringify(i)}]`,
933✔
828
        '(context||0)'
829
      )}, ${buildString(defaultValue, buildState)})))`
830
    }
831
    return `(methods.preventFunctions(context${pieces
4,185✔
832
      .map((i) => `?.[${JSON.stringify(i)}]`)
4,665✔
833
      .join('')} ?? ${buildString(defaultValue, buildState)}))`
834
  }
835
  return false
×
836
}
837

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

841
export default {
842
  ...defaultMethods
843
}
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