@jbrowse/jexl 5.0.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +8 -3
  2. package/dist/Expression.d.ts +0 -12
  3. package/dist/Expression.js +0 -16
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.js +12 -16
  6. package/dist/Jexl.js.map +1 -1
  7. package/dist/Lexer.d.ts +12 -94
  8. package/dist/Lexer.js +41 -142
  9. package/dist/Lexer.js.map +1 -1
  10. package/dist/analyze.d.ts +2 -0
  11. package/dist/analyze.js +7 -7
  12. package/dist/analyze.js.map +1 -1
  13. package/dist/check.d.ts +0 -5
  14. package/dist/check.js +62 -240
  15. package/dist/check.js.map +1 -1
  16. package/dist/collections.d.ts +7 -0
  17. package/dist/collections.js +9 -2
  18. package/dist/collections.js.map +1 -1
  19. package/dist/conditions.d.ts +1 -1
  20. package/dist/conditions.js +28 -50
  21. package/dist/conditions.js.map +1 -1
  22. package/dist/errors.d.ts +5 -0
  23. package/dist/errors.js +8 -0
  24. package/dist/errors.js.map +1 -1
  25. package/dist/evaluator/compile.js +37 -24
  26. package/dist/evaluator/compile.js.map +1 -1
  27. package/dist/grammar.d.ts +2 -2
  28. package/dist/grammar.js +1 -8
  29. package/dist/grammar.js.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +3 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/operators.d.ts +5 -0
  34. package/dist/operators.js +9 -5
  35. package/dist/operators.js.map +1 -1
  36. package/dist/parser/Parser.d.ts +11 -12
  37. package/dist/parser/Parser.js +4 -10
  38. package/dist/parser/Parser.js.map +1 -1
  39. package/dist/print.d.ts +9 -0
  40. package/dist/print.js +183 -0
  41. package/dist/print.js.map +1 -0
  42. package/dist/types.d.ts +18 -21
  43. package/esm/Expression.d.ts +0 -12
  44. package/esm/Expression.js +0 -16
  45. package/esm/Expression.js.map +1 -1
  46. package/esm/Jexl.js +12 -16
  47. package/esm/Jexl.js.map +1 -1
  48. package/esm/Lexer.d.ts +12 -94
  49. package/esm/Lexer.js +40 -142
  50. package/esm/Lexer.js.map +1 -1
  51. package/esm/analyze.d.ts +2 -0
  52. package/esm/analyze.js +6 -7
  53. package/esm/analyze.js.map +1 -1
  54. package/esm/check.d.ts +0 -5
  55. package/esm/check.js +29 -206
  56. package/esm/check.js.map +1 -1
  57. package/esm/collections.d.ts +7 -0
  58. package/esm/collections.js +8 -1
  59. package/esm/collections.js.map +1 -1
  60. package/esm/conditions.d.ts +1 -1
  61. package/esm/conditions.js +27 -49
  62. package/esm/conditions.js.map +1 -1
  63. package/esm/errors.d.ts +5 -0
  64. package/esm/errors.js +7 -0
  65. package/esm/errors.js.map +1 -1
  66. package/esm/evaluator/compile.js +37 -24
  67. package/esm/evaluator/compile.js.map +1 -1
  68. package/esm/grammar.d.ts +2 -2
  69. package/esm/grammar.js +1 -8
  70. package/esm/grammar.js.map +1 -1
  71. package/esm/index.d.ts +2 -1
  72. package/esm/index.js +2 -1
  73. package/esm/index.js.map +1 -1
  74. package/esm/operators.d.ts +5 -0
  75. package/esm/operators.js +8 -5
  76. package/esm/operators.js.map +1 -1
  77. package/esm/parser/Parser.d.ts +11 -12
  78. package/esm/parser/Parser.js +4 -8
  79. package/esm/parser/Parser.js.map +1 -1
  80. package/esm/print.d.ts +9 -0
  81. package/esm/print.js +177 -0
  82. package/esm/print.js.map +1 -0
  83. package/esm/types.d.ts +18 -21
  84. package/package.json +2 -1
  85. package/src/Expression.ts +0 -20
  86. package/src/Jexl.ts +13 -20
  87. package/src/Lexer.ts +42 -144
  88. package/src/analyze.ts +10 -10
  89. package/src/check.ts +48 -250
  90. package/src/collections.ts +11 -1
  91. package/src/conditions.ts +39 -62
  92. package/src/errors.ts +10 -0
  93. package/src/evaluator/compile.ts +49 -36
  94. package/src/grammar.ts +3 -9
  95. package/src/index.ts +2 -1
  96. package/src/operators.ts +10 -6
  97. package/src/parser/Parser.ts +13 -19
  98. package/src/print.ts +203 -0
  99. package/src/types.ts +19 -23
package/src/check.ts CHANGED
@@ -3,15 +3,18 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
+ import { literalValue } from './analyze.ts'
7
+ import { unknownNode } from './errors.ts'
8
+ import { toRegExp } from './operators.ts'
9
+ import { isName, key, print, quote } from './print.ts'
10
+
6
11
  import type { AnalyzeOptions } from './analyze.ts'
7
12
  import type {
8
13
  AstNode,
9
- AstNodeUnion,
10
14
  BinaryExpression,
11
15
  FunctionCall,
12
16
  Identifier,
13
- Lambda,
14
- Literal
17
+ Lambda
15
18
  } from './types.ts'
16
19
 
17
20
  /**
@@ -148,8 +151,6 @@ export interface CheckResult {
148
151
  type: Type
149
152
  }
150
153
 
151
- type Node = AstNodeUnion
152
-
153
154
  export const UNKNOWN: Type = { kind: 'unknown' }
154
155
  const UNDEFINED: Type = { kind: 'undefined' }
155
156
  const BOOLEAN: Type = { kind: 'boolean' }
@@ -160,203 +161,6 @@ const MAX_VALUES = 64
160
161
  const ARITHMETIC = new Set(['-', '*', '/', '//', '%', '^'])
161
162
  const ORDER = new Set(['<', '<=', '>', '>='])
162
163
  const EQUALITY = new Set(['==', '!='])
163
- const PRECEDENCE: Record<string, number> = {
164
- '=': 2,
165
- '||': 10,
166
- '??': 10,
167
- '&&': 11,
168
- '==': 20,
169
- '!=': 20,
170
- '~': 20,
171
- '!~': 20,
172
- '<': 20,
173
- '<=': 20,
174
- '>': 20,
175
- '>=': 20,
176
- in: 20,
177
- '+': 30,
178
- '-': 30,
179
- '*': 40,
180
- '/': 40,
181
- '//': 40,
182
- '%': 50,
183
- '^': 50
184
- }
185
-
186
- const RIGHT_ASSOCIATIVE = new Set(['^'])
187
- const LOGICAL = new Set(['||', '&&'])
188
-
189
- // how tightly each form binds, mirroring the Parser's levels: `;`, then `=`
190
- // and lambdas, then `?:`, then binary operators, then prefix operators
191
- const SEQUENCE = 0
192
- const ASSIGNMENT = 1
193
- const CONDITIONAL = 2
194
- const CUSTOM_BINARY = 3
195
- const UNARY = 100
196
-
197
- const letter = String.raw`a-zA-Zа-яА-Я_À-ÖØ-öø-ÿ$`
198
- const IDENTIFIER = new RegExp(`^[${letter}][${letter}0-9]*$`)
199
- const WORDS = new Set(['in', 'true', 'false', 'null'])
200
-
201
- function isName(text: string) {
202
- return IDENTIFIER.test(text) && !WORDS.has(text)
203
- }
204
-
205
- function key(name: string) {
206
- return isName(name) ? `.${name}` : `[${quote(name)}]`
207
- }
208
-
209
- function quote(text: string) {
210
- return `'${text.replaceAll('\\', '\\\\').replaceAll("'", String.raw`\'`)}'`
211
- }
212
-
213
- function precedenceOf(node: Node) {
214
- switch (node.type) {
215
- case 'SequenceExpression': {
216
- return SEQUENCE
217
- }
218
- case 'AssignmentExpression':
219
- case 'Lambda': {
220
- return ASSIGNMENT
221
- }
222
- case 'ConditionalExpression': {
223
- // with no alternate, it ends only before `)`, `]`, `}`, `,`, `;` or the end
224
- return node.alternate ? CONDITIONAL : SEQUENCE
225
- }
226
- case 'BinaryExpression': {
227
- return PRECEDENCE[node.operator] ?? CUSTOM_BINARY
228
- }
229
- case 'UnaryExpression': {
230
- return UNARY
231
- }
232
- default: {
233
- return Infinity
234
- }
235
- }
236
- }
237
-
238
- function within(ast: AstNode, least: number) {
239
- const text = print(ast)
240
- return precedenceOf(ast as Node) < least ? `(${text})` : text
241
- }
242
-
243
- function binaryOperand(parent: BinaryExpression, ast: AstNode, left: boolean) {
244
- const own = PRECEDENCE[parent.operator]
245
- if (own === undefined) {
246
- return within(ast, UNARY)
247
- }
248
- const child = ast as Node
249
- const theirs = precedenceOf(child)
250
- const chained =
251
- child.type === 'BinaryExpression' &&
252
- RIGHT_ASSOCIATIVE.has(parent.operator) &&
253
- RIGHT_ASSOCIATIVE.has(child.operator)
254
- const mixesNullish =
255
- child.type === 'BinaryExpression' &&
256
- ((parent.operator === '??' && LOGICAL.has(child.operator)) ||
257
- (child.operator === '??' && LOGICAL.has(parent.operator)))
258
- const text = print(ast)
259
- return theirs < own || (theirs === own && left === chained) || mixesNullish
260
- ? `(${text})`
261
- : text
262
- }
263
-
264
- /**
265
- * Renders a tree back to expression text, for messages and suggestions. The
266
- * text parses back to the same tree.
267
- */
268
- export function print(ast: AstNode): string {
269
- const node = ast as Node
270
- switch (node.type) {
271
- case 'Literal': {
272
- const { value } = node
273
- return typeof value === 'string'
274
- ? quote(value)
275
- : Object.is(value, -0)
276
- ? '-0'
277
- : String(value)
278
- }
279
- case 'Identifier': {
280
- return node.from
281
- ? within(node.from, Infinity) + key(node.value)
282
- : node.value
283
- }
284
- case 'FilterExpression': {
285
- return `${within(node.subject, Infinity)}[${within(node.expr, ASSIGNMENT)}]`
286
- }
287
- case 'BinaryExpression': {
288
- return `${binaryOperand(node, node.left, true)} ${node.operator} ${binaryOperand(node, node.right!, false)}`
289
- }
290
- case 'UnaryExpression': {
291
- const { operator } = node
292
- const right = within(node.right!, UNARY)
293
- // the Lexer folds a minus before a digit into the number
294
- if (operator === '-' && /^\d/.test(right)) {
295
- return `-(${right})`
296
- }
297
- return IDENTIFIER.test(operator)
298
- ? `${operator} ${right}`
299
- : operator + right
300
- }
301
- case 'ConditionalExpression': {
302
- const test = within(node.test, CUSTOM_BINARY)
303
- const consequent = node.consequent
304
- ? ` ${within(node.consequent, ASSIGNMENT)} `
305
- : ''
306
- const alternate = node.alternate
307
- ? ` ${within(node.alternate, ASSIGNMENT)}`
308
- : ''
309
- return `${test} ?${consequent}:${alternate}`
310
- }
311
- case 'FunctionCall': {
312
- const args = node.args.map((arg) => within(arg, ASSIGNMENT))
313
- return `${node.name}(${args.join(', ')})`
314
- }
315
- case 'ArrayLiteral': {
316
- const items = node.value.map((item) => within(item, ASSIGNMENT))
317
- return `[${items.join(', ')}]`
318
- }
319
- case 'ObjectLiteral': {
320
- const entries = Object.entries(node.value).map(
321
- ([name, value]) =>
322
- `${isName(name) ? name : quote(name)}: ${within(value, ASSIGNMENT)}`
323
- )
324
- return `{${entries.join(', ')}}`
325
- }
326
- case 'TemplateLiteral': {
327
- const parts = node.parts.map((part) =>
328
- part.type === 'static'
329
- ? part.value
330
- .replaceAll('\\', '\\\\')
331
- .replaceAll('`', '\\`')
332
- .replaceAll('${', '\\${')
333
- : `\${${print(part.value)}}`
334
- )
335
- return `\`${parts.join('')}\``
336
- }
337
- case 'SequenceExpression': {
338
- const { expressions } = node
339
- const text = expressions
340
- .map((expr) => within(expr, ASSIGNMENT))
341
- .join('; ')
342
- return expressions.length === 1 ? `${text};` : text
343
- }
344
- case 'AssignmentExpression': {
345
- return `${node.left.value} = ${within(node.right!, ASSIGNMENT)}`
346
- }
347
- case 'Lambda': {
348
- const params =
349
- node.params.length === 1
350
- ? node.params[0]
351
- : `(${node.params.join(', ')})`
352
- return `${params} => ${within(node.body, ASSIGNMENT)}`
353
- }
354
- default: {
355
- return '?'
356
- }
357
- }
358
- }
359
-
360
164
  function distance(a: string, b: string) {
361
165
  let before: number[] = []
362
166
  let previous = Array.from({ length: b.length + 1 }, (_, i) => i)
@@ -487,7 +291,7 @@ function isBoxed(type: Type): type is OfKind<'list'> {
487
291
 
488
292
  function isPattern(pattern: string) {
489
293
  try {
490
- new RegExp(pattern.startsWith('(?i)') ? pattern.slice(4) : pattern)
294
+ toRegExp(pattern)
491
295
  return true
492
296
  } catch {
493
297
  return false
@@ -775,10 +579,9 @@ function negate(type: Type): Type {
775
579
  }
776
580
 
777
581
  function paramName(subject: AstNode, context: AstNode) {
778
- const node = subject as Node
779
582
  const base =
780
- node.type === 'Identifier'
781
- ? node.value.toLowerCase().replaceAll(/\W/g, '')
583
+ subject.type === 'Identifier'
584
+ ? subject.value.toLowerCase().replaceAll(/\W/g, '')
782
585
  : ''
783
586
  const name = isName(base) ? base : 'x'
784
587
  const text = print(context)
@@ -789,10 +592,6 @@ function paramName(subject: AstNode, context: AstNode) {
789
592
  return candidate
790
593
  }
791
594
 
792
- function literalValue(node: AstNode) {
793
- return node.type === 'Literal' ? (node as Literal).value : undefined
794
- }
795
-
796
595
  function describe(type: Type): string {
797
596
  switch (type.kind) {
798
597
  case 'list': {
@@ -1032,27 +831,20 @@ export function check(
1032
831
  }
1033
832
  }
1034
833
 
1035
- function chain(node: Identifier): { base: AstNode; names: string[] } {
834
+ function dottedKey(node: Identifier): Type | undefined {
1036
835
  const names = [node.value]
1037
- let base: AstNode = node
1038
- while (base.type === 'Identifier' && (base as Identifier).from) {
1039
- base = (base as Identifier).from!
1040
- if (base.type === 'Identifier') {
1041
- names.unshift((base as Identifier).value)
836
+ let base = node
837
+ while (base.from) {
838
+ if (base.from.type !== 'Identifier') {
839
+ return undefined
1042
840
  }
841
+ base = base.from
842
+ names.unshift(base.value)
1043
843
  }
1044
- return { base, names }
1045
- }
1046
-
1047
- function dottedKey(node: Identifier): Type | undefined {
1048
- if (!node.from) {
1049
- return undefined
1050
- }
1051
- const { base, names } = chain(node)
1052
- if ((base as Identifier).from) {
844
+ if (base === node) {
1053
845
  return undefined
1054
846
  }
1055
- let type = variable(base as Identifier, false)
847
+ let type = variable(base, false)
1056
848
  let subjectNode: AstNode = base
1057
849
  for (let i = 1; i < names.length; i++) {
1058
850
  const record = type.kind === 'record' ? type : undefined
@@ -1090,7 +882,7 @@ export function check(
1090
882
  type: 'Identifier',
1091
883
  value: names[i]!,
1092
884
  from: subjectNode
1093
- } as Identifier
885
+ }
1094
886
  }
1095
887
  return undefined
1096
888
  }
@@ -1134,34 +926,45 @@ export function check(
1134
926
  return UNKNOWN
1135
927
  }
1136
928
 
1137
- function hyphenated(node: AstNodeUnion): Type | undefined {
929
+ function hyphenated(node: AstNode): Type | undefined {
1138
930
  if (
1139
931
  node.type !== 'BinaryExpression' ||
1140
932
  node.operator !== '-' ||
1141
933
  node.left.type !== 'Identifier' ||
1142
- node.right?.type !== 'Identifier' ||
1143
- (node.right as Identifier).from
934
+ node.right.type !== 'Identifier' ||
935
+ node.right.from
936
+ ) {
937
+ return undefined
938
+ }
939
+ const left = node.left
940
+ const name = left.value
941
+ if (
942
+ !left.from &&
943
+ (scope.has(name) || name === row || (env && Object.hasOwn(env, name)))
1144
944
  ) {
1145
945
  return undefined
1146
946
  }
1147
- const left = node.left as Identifier
1148
947
  const reported = diagnostics.length
1149
948
  const subject = left.from ? infer(left.from) : root
1150
949
  diagnostics.length = reported
1151
- if (subject.kind !== 'record' || subject.fields.has(left.value)) {
950
+ if (subject.kind !== 'record' || subject.fields.has(name)) {
1152
951
  return undefined
1153
952
  }
1154
- const whole = `${left.value}-${(node.right as Identifier).value}`
953
+ const whole = `${name}-${node.right.value}`
1155
954
  const found = subject.fields.get(whole)
1156
955
  if (!found) {
1157
956
  return undefined
1158
957
  }
958
+ // a bare name can't hold a hyphen, so only a row variable can spell it
959
+ const holder: AstNode | undefined =
960
+ left.from ??
961
+ (row === undefined ? undefined : { type: 'Identifier', value: row })
1159
962
  report(
1160
963
  'unknown-field',
1161
964
  'warning',
1162
965
  node,
1163
966
  `${print(node)} subtracts; ${whole} is one field`,
1164
- [text(left.from, whole)]
967
+ holder ? [text(holder, whole)] : []
1165
968
  )
1166
969
  return found
1167
970
  }
@@ -1244,11 +1047,9 @@ export function check(
1244
1047
  }
1245
1048
  }
1246
1049
 
1247
- function binary(
1248
- node: Extract<AstNodeUnion, { type: 'BinaryExpression' }>
1249
- ): Type {
1050
+ function binary(node: BinaryExpression): Type {
1250
1051
  const { operator } = node
1251
- const rightNode = node.right!
1052
+ const rightNode = node.right
1252
1053
  if (operator === '&&' || operator === '||' || operator === '??') {
1253
1054
  const left = infer(node.left)
1254
1055
  const right = infer(rightNode)
@@ -1343,7 +1144,7 @@ export function check(
1343
1144
  if (EQUALITY.has(operator)) {
1344
1145
  const leftLiteral = literalOf(leftItem)
1345
1146
  const rightLiteral = literalOf(rightItem)
1346
- if (rightLiteral !== undefined && node.right?.type === 'Literal') {
1147
+ if (rightLiteral !== undefined && node.right.type === 'Literal') {
1347
1148
  compareLiteral(leftItem, rightLiteral, node)
1348
1149
  } else if (leftLiteral !== undefined && node.left.type === 'Literal') {
1349
1150
  compareLiteral(rightItem, leftLiteral, node)
@@ -1430,7 +1231,7 @@ export function check(
1430
1231
  type: 'Identifier',
1431
1232
  value: name,
1432
1233
  from: subjectNode
1433
- } as Identifier
1234
+ }
1434
1235
  }
1435
1236
  for (const name of prefix) {
1436
1237
  step(String(name), node)
@@ -1457,9 +1258,7 @@ export function check(
1457
1258
  node,
1458
1259
  `no function ${name}` +
1459
1260
  (near.length ? `; did you mean ${near.join(', ')}?` : ''),
1460
- near.map((candidate) =>
1461
- print({ ...node, name: candidate } as FunctionCall)
1462
- )
1261
+ near.map((candidate) => print({ ...node, name: candidate }))
1463
1262
  )
1464
1263
  }
1465
1264
  return UNKNOWN
@@ -1486,10 +1285,10 @@ export function check(
1486
1285
  }
1487
1286
  const accepted: readonly ParamType[] =
1488
1287
  typeof declared === 'string' ? [declared] : declared
1489
- if ((arg as Node).type === 'Lambda') {
1288
+ if (arg.type === 'Lambda') {
1490
1289
  const item =
1491
1290
  lastCollection?.kind === 'list' ? lastCollection.of : UNKNOWN
1492
- types.push(lambda(arg as Lambda, lambdaParams(name, item)))
1291
+ types.push(lambda(arg, lambdaParams(name, item)))
1493
1292
  continue
1494
1293
  }
1495
1294
  const type = infer(arg)
@@ -1537,8 +1336,7 @@ export function check(
1537
1336
  return returns ?? UNKNOWN
1538
1337
  }
1539
1338
 
1540
- function infer(ast: AstNode): Type {
1541
- const node = ast as Node
1339
+ function infer(node: AstNode): Type {
1542
1340
  switch (node.type) {
1543
1341
  case 'Literal': {
1544
1342
  const { value } = node
@@ -1599,7 +1397,7 @@ export function check(
1599
1397
  return binary(node)
1600
1398
  }
1601
1399
  case 'UnaryExpression': {
1602
- const right = infer(node.right!)
1400
+ const right = infer(node.right)
1603
1401
  if (node.operator === '!') {
1604
1402
  return BOOLEAN
1605
1403
  }
@@ -1664,7 +1462,7 @@ export function check(
1664
1462
  return last
1665
1463
  }
1666
1464
  case 'AssignmentExpression': {
1667
- const value = infer(node.right!)
1465
+ const value = infer(node.right)
1668
1466
  scope.set(node.left.value, value)
1669
1467
  return value
1670
1468
  }
@@ -1672,7 +1470,7 @@ export function check(
1672
1470
  return lambda(node)
1673
1471
  }
1674
1472
  default: {
1675
- throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
1473
+ return unknownNode(node)
1676
1474
  }
1677
1475
  }
1678
1476
  }
@@ -34,13 +34,23 @@ function isPlainObject(value: JexlValue): value is Record<string, JexlValue> {
34
34
  return proto === Object.prototype || proto === null
35
35
  }
36
36
 
37
+ /**
38
+ * Marks the functions lambdas evaluate to. A collection function calls only
39
+ * these, never a function an expression reads, since reads reach host
40
+ * functions: `reduce(['x', body], ''.constructor.constructor)` built a
41
+ * function from `body`, and `map` then ran it.
42
+ */
43
+ export const LAMBDA = Symbol('lambda')
44
+
45
+ type Lambda = JexlFunction & { [LAMBDA]?: true }
46
+
37
47
  const identity = (value: JexlValue) => value
38
48
 
39
49
  function callback(name: string, fn: JexlValue): JexlFunction {
40
50
  if (fn === undefined) {
41
51
  return identity
42
52
  }
43
- if (typeof fn !== 'function') {
53
+ if (typeof fn !== 'function' || !(fn as Lambda)[LAMBDA]) {
44
54
  throw new TypeError(`${name}() expects a lambda, such as x => x > 1`)
45
55
  }
46
56
  return fn
package/src/conditions.ts CHANGED
@@ -3,11 +3,11 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import { analyze } from './analyze.ts'
7
- import { print } from './check.ts'
6
+ import { analyze, literalValue } from './analyze.ts'
7
+ import { print } from './print.ts'
8
8
 
9
9
  import type { PathKey } from './analyze.ts'
10
- import type { AstNode, AstNodeUnion } from './types.ts'
10
+ import type { AstNode } from './types.ts'
11
11
 
12
12
  export type Scalar = string | number | boolean | null
13
13
 
@@ -50,38 +50,25 @@ const FLIP: Record<string, Condition['op']> = {
50
50
  '>=': '<='
51
51
  }
52
52
 
53
- function scalar(ast: AstNode): { value: Scalar } | undefined {
54
- const node = ast as AstNodeUnion
55
- if (node.type === 'Literal') {
56
- return { value: node.value }
57
- }
58
- return node.type === 'TemplateLiteral' &&
59
- node.parts.every((part) => part.type === 'static')
60
- ? { value: node.parts.map((part) => part.value).join('') }
53
+ function scalars(nodes: AstNode[]) {
54
+ const values = nodes.map(literalValue)
55
+ return values.every((value): value is Scalar => value !== undefined)
56
+ ? values
61
57
  : undefined
62
58
  }
63
59
 
64
- function isRow(ast: AstNode | undefined, row: string) {
65
- const node = ast as AstNodeUnion | undefined
60
+ function isRow(node: AstNode | undefined, row: string) {
66
61
  return node?.type === 'Identifier' && !node.from && node.value === row
67
62
  }
68
63
 
69
- function subjectOf(ast: AstNode, opts: ConditionOptions): Subject | undefined {
70
- const node = ast as AstNodeUnion
64
+ function subjectOf(node: AstNode, opts: ConditionOptions): Subject | undefined {
71
65
  if (
72
66
  node.type === 'FunctionCall' &&
73
67
  opts.calls?.includes(node.name) &&
74
68
  isRow(node.args[0], opts.row)
75
69
  ) {
76
- const rest = node.args.slice(1).map(scalar)
77
- return rest.every(Boolean)
78
- ? {
79
- kind: 'call',
80
- name: node.name,
81
- args: rest.map((arg) => arg!.value),
82
- node
83
- }
84
- : undefined
70
+ const args = scalars(node.args.slice(1))
71
+ return args && { kind: 'call', name: node.name, args, node }
85
72
  }
86
73
  const accessor =
87
74
  node.type === 'FunctionCall' &&
@@ -109,12 +96,11 @@ function subjectOf(ast: AstNode, opts: ConditionOptions): Subject | undefined {
109
96
  }
110
97
 
111
98
  function condition(
112
- ast: AstNode,
99
+ node: AstNode,
113
100
  opts: ConditionOptions
114
101
  ): Condition | undefined {
115
- const node = ast as AstNodeUnion
116
102
  if (node.type === 'UnaryExpression' && node.operator === '!') {
117
- const inner = condition(node.right!, opts)
103
+ const inner = condition(node.right, opts)
118
104
  // only forms whose negation reads as a condition of its own; `!(QUAL > 30)`
119
105
  // holds where QUAL is missing, which `QUAL <= 30` does not
120
106
  return inner?.op === 'in' || inner?.op === 'has' || inner?.op === 'set'
@@ -126,43 +112,36 @@ function condition(
126
112
  return subject && { subject, op: 'set' }
127
113
  }
128
114
  const { operator, left } = node
129
- const right = node.right!
115
+ const right = node.right
130
116
  if (COMPARE.has(operator)) {
131
117
  const subject = subjectOf(left, opts)
132
- const value = scalar(right)
133
- if (subject && value) {
134
- return { subject, op: operator as '==', value: value.value }
118
+ const value = literalValue(right)
119
+ if (subject && value !== undefined) {
120
+ return { subject, op: operator as '==', value }
135
121
  }
136
122
  const flipped = subjectOf(right, opts)
137
- const leftValue = scalar(left)
138
- return flipped && leftValue
139
- ? {
140
- subject: flipped,
141
- op: FLIP[operator] as '==',
142
- value: leftValue.value
143
- }
123
+ const leftValue = literalValue(left)
124
+ return flipped && leftValue !== undefined
125
+ ? { subject: flipped, op: FLIP[operator] as '==', value: leftValue }
144
126
  : undefined
145
127
  }
146
128
  if (operator === '~' || operator === '!~') {
147
129
  const subject = subjectOf(left, opts)
148
- const value = scalar(right)
149
- return subject && typeof value?.value === 'string'
150
- ? { subject, op: operator, value: value.value }
130
+ const value = literalValue(right)
131
+ return subject && typeof value === 'string'
132
+ ? { subject, op: operator, value }
151
133
  : undefined
152
134
  }
153
135
  if (operator === 'in') {
154
136
  const subject = subjectOf(left, opts)
155
- const list = right as AstNodeUnion
156
- if (subject && list.type === 'ArrayLiteral') {
157
- const values = list.value.map(scalar)
158
- return values.length > 0 && values.every(Boolean)
159
- ? { subject, op: 'in', value: values.map((value) => value!.value) }
160
- : undefined
137
+ if (subject && right.type === 'ArrayLiteral') {
138
+ const values = scalars(right.value)
139
+ return values?.length ? { subject, op: 'in', value: values } : undefined
161
140
  }
162
- const key = scalar(left)
141
+ const key = literalValue(left)
163
142
  const container = subjectOf(right, opts)
164
- return container && typeof key?.value === 'string'
165
- ? { subject: container, op: 'has', value: key.value }
143
+ return container && typeof key === 'string'
144
+ ? { subject: container, op: 'has', value: key }
166
145
  : undefined
167
146
  }
168
147
  return undefined
@@ -176,37 +155,35 @@ function condition(
176
155
  * misread.
177
156
  */
178
157
  export function conditions(
179
- ast: AstNode | null,
158
+ node: AstNode | null,
180
159
  opts: ConditionOptions
181
160
  ): Condition[] | undefined {
182
- const node = ast as AstNodeUnion | null
183
161
  if (!node) {
184
162
  return undefined
185
163
  }
186
164
  if (node.type === 'BinaryExpression' && node.operator === '&&') {
187
165
  const left = conditions(node.left, opts)
188
- const right = left && conditions(node.right!, opts)
166
+ const right = left && conditions(node.right, opts)
189
167
  return left && right && [...left, ...right]
190
168
  }
191
169
  const found = condition(node, opts)
192
170
  return found && [found]
193
171
  }
194
172
 
195
- const literal = (value: Scalar) =>
196
- print({ type: 'Literal', value } as AstNodeUnion)
173
+ const literal = (value: Scalar) => print({ type: 'Literal', value })
197
174
 
198
175
  /** A subject reading `path` off the row, for a condition built from a picker. */
199
176
  export function pathSubject(row: string, path: PathKey[]): Subject {
200
- let node: AstNode = { type: 'Identifier', value: row } as AstNodeUnion
177
+ let node: AstNode = { type: 'Identifier', value: row }
201
178
  for (const key of path) {
202
179
  node =
203
180
  typeof key === 'string'
204
- ? ({ type: 'Identifier', value: key, from: node } as AstNodeUnion)
205
- : ({
181
+ ? { type: 'Identifier', value: key, from: node }
182
+ : {
206
183
  type: 'FilterExpression',
207
184
  subject: node,
208
185
  expr: { type: 'Literal', value: key }
209
- } as AstNodeUnion)
186
+ }
210
187
  }
211
188
  return { kind: 'path', path, node }
212
189
  }
@@ -217,14 +194,14 @@ export function callSubject(
217
194
  name: string,
218
195
  args: Scalar[] = []
219
196
  ): Subject {
220
- const node = {
197
+ const node: AstNode = {
221
198
  type: 'FunctionCall',
222
199
  name,
223
200
  args: [
224
201
  { type: 'Identifier', value: row },
225
- ...args.map((value) => ({ type: 'Literal', value }))
202
+ ...args.map((value): AstNode => ({ type: 'Literal', value }))
226
203
  ]
227
- } as AstNodeUnion
204
+ }
228
205
  return { kind: 'call', name, args, node }
229
206
  }
230
207
 
package/src/errors.ts CHANGED
@@ -13,3 +13,13 @@ export class JexlSyntaxError extends Error {
13
13
  this.offset = offset
14
14
  }
15
15
  }
16
+
17
+ /**
18
+ * Throws for a node no case of a walk handled. Typing it `never` makes a new
19
+ * node type a compile error in every walk that doesn't handle it yet.
20
+ */
21
+ export function unknownNode(node: never): never {
22
+ throw new Error(
23
+ `Corrupt AST: unknown node type '${(node as { type: string }).type}'`
24
+ )
25
+ }