@jbrowse/jexl 5.0.2 → 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.
- package/README.md +4 -1
- package/dist/Jexl.js +12 -16
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +12 -94
- package/dist/Lexer.js +41 -142
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.js +7 -7
- package/dist/analyze.js.map +1 -1
- package/dist/check.js +30 -30
- package/dist/check.js.map +1 -1
- package/dist/collections.d.ts +7 -0
- package/dist/collections.js +9 -2
- package/dist/collections.js.map +1 -1
- package/dist/conditions.d.ts +1 -1
- package/dist/conditions.js +25 -47
- package/dist/conditions.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/evaluator/compile.js +37 -24
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +1 -8
- package/dist/grammar.js.map +1 -1
- package/dist/operators.d.ts +5 -0
- package/dist/operators.js +9 -5
- package/dist/operators.js.map +1 -1
- package/dist/parser/Parser.d.ts +11 -12
- package/dist/parser/Parser.js +4 -10
- package/dist/parser/Parser.js.map +1 -1
- package/dist/print.d.ts +1 -1
- package/dist/print.js +41 -47
- package/dist/print.js.map +1 -1
- package/dist/types.d.ts +18 -21
- package/esm/Jexl.js +12 -16
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +12 -94
- package/esm/Lexer.js +40 -142
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +2 -0
- package/esm/analyze.js +6 -7
- package/esm/analyze.js.map +1 -1
- package/esm/check.js +28 -28
- package/esm/check.js.map +1 -1
- package/esm/collections.d.ts +7 -0
- package/esm/collections.js +8 -1
- package/esm/collections.js.map +1 -1
- package/esm/conditions.d.ts +1 -1
- package/esm/conditions.js +26 -48
- package/esm/conditions.js.map +1 -1
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +7 -0
- package/esm/errors.js.map +1 -1
- package/esm/evaluator/compile.js +37 -24
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +2 -2
- package/esm/grammar.js +1 -8
- package/esm/grammar.js.map +1 -1
- package/esm/operators.d.ts +5 -0
- package/esm/operators.js +8 -5
- package/esm/operators.js.map +1 -1
- package/esm/parser/Parser.d.ts +11 -12
- package/esm/parser/Parser.js +4 -8
- package/esm/parser/Parser.js.map +1 -1
- package/esm/print.d.ts +1 -1
- package/esm/print.js +41 -47
- package/esm/print.js.map +1 -1
- package/esm/types.d.ts +18 -21
- package/package.json +1 -1
- package/src/Jexl.ts +13 -20
- package/src/Lexer.ts +42 -144
- package/src/analyze.ts +10 -10
- package/src/check.ts +47 -53
- package/src/collections.ts +11 -1
- package/src/conditions.ts +38 -61
- package/src/errors.ts +10 -0
- package/src/evaluator/compile.ts +49 -36
- package/src/grammar.ts +3 -9
- package/src/operators.ts +10 -6
- package/src/parser/Parser.ts +13 -19
- package/src/print.ts +50 -52
- package/src/types.ts +19 -23
package/src/check.ts
CHANGED
|
@@ -3,16 +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'
|
|
6
9
|
import { isName, key, print, quote } from './print.ts'
|
|
7
10
|
|
|
8
11
|
import type { AnalyzeOptions } from './analyze.ts'
|
|
9
12
|
import type {
|
|
10
13
|
AstNode,
|
|
11
|
-
|
|
14
|
+
BinaryExpression,
|
|
12
15
|
FunctionCall,
|
|
13
16
|
Identifier,
|
|
14
|
-
Lambda
|
|
15
|
-
Literal
|
|
17
|
+
Lambda
|
|
16
18
|
} from './types.ts'
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -149,8 +151,6 @@ export interface CheckResult {
|
|
|
149
151
|
type: Type
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
type Node = AstNodeUnion
|
|
153
|
-
|
|
154
154
|
export const UNKNOWN: Type = { kind: 'unknown' }
|
|
155
155
|
const UNDEFINED: Type = { kind: 'undefined' }
|
|
156
156
|
const BOOLEAN: Type = { kind: 'boolean' }
|
|
@@ -291,7 +291,7 @@ function isBoxed(type: Type): type is OfKind<'list'> {
|
|
|
291
291
|
|
|
292
292
|
function isPattern(pattern: string) {
|
|
293
293
|
try {
|
|
294
|
-
|
|
294
|
+
toRegExp(pattern)
|
|
295
295
|
return true
|
|
296
296
|
} catch {
|
|
297
297
|
return false
|
|
@@ -579,10 +579,9 @@ function negate(type: Type): Type {
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
function paramName(subject: AstNode, context: AstNode) {
|
|
582
|
-
const node = subject as Node
|
|
583
582
|
const base =
|
|
584
|
-
|
|
585
|
-
?
|
|
583
|
+
subject.type === 'Identifier'
|
|
584
|
+
? subject.value.toLowerCase().replaceAll(/\W/g, '')
|
|
586
585
|
: ''
|
|
587
586
|
const name = isName(base) ? base : 'x'
|
|
588
587
|
const text = print(context)
|
|
@@ -593,10 +592,6 @@ function paramName(subject: AstNode, context: AstNode) {
|
|
|
593
592
|
return candidate
|
|
594
593
|
}
|
|
595
594
|
|
|
596
|
-
function literalValue(node: AstNode) {
|
|
597
|
-
return node.type === 'Literal' ? (node as Literal).value : undefined
|
|
598
|
-
}
|
|
599
|
-
|
|
600
595
|
function describe(type: Type): string {
|
|
601
596
|
switch (type.kind) {
|
|
602
597
|
case 'list': {
|
|
@@ -836,27 +831,20 @@ export function check(
|
|
|
836
831
|
}
|
|
837
832
|
}
|
|
838
833
|
|
|
839
|
-
function
|
|
834
|
+
function dottedKey(node: Identifier): Type | undefined {
|
|
840
835
|
const names = [node.value]
|
|
841
|
-
let base
|
|
842
|
-
while (base.
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
names.unshift((base as Identifier).value)
|
|
836
|
+
let base = node
|
|
837
|
+
while (base.from) {
|
|
838
|
+
if (base.from.type !== 'Identifier') {
|
|
839
|
+
return undefined
|
|
846
840
|
}
|
|
841
|
+
base = base.from
|
|
842
|
+
names.unshift(base.value)
|
|
847
843
|
}
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function dottedKey(node: Identifier): Type | undefined {
|
|
852
|
-
if (!node.from) {
|
|
853
|
-
return undefined
|
|
854
|
-
}
|
|
855
|
-
const { base, names } = chain(node)
|
|
856
|
-
if ((base as Identifier).from) {
|
|
844
|
+
if (base === node) {
|
|
857
845
|
return undefined
|
|
858
846
|
}
|
|
859
|
-
let type = variable(base
|
|
847
|
+
let type = variable(base, false)
|
|
860
848
|
let subjectNode: AstNode = base
|
|
861
849
|
for (let i = 1; i < names.length; i++) {
|
|
862
850
|
const record = type.kind === 'record' ? type : undefined
|
|
@@ -894,7 +882,7 @@ export function check(
|
|
|
894
882
|
type: 'Identifier',
|
|
895
883
|
value: names[i]!,
|
|
896
884
|
from: subjectNode
|
|
897
|
-
}
|
|
885
|
+
}
|
|
898
886
|
}
|
|
899
887
|
return undefined
|
|
900
888
|
}
|
|
@@ -938,34 +926,45 @@ export function check(
|
|
|
938
926
|
return UNKNOWN
|
|
939
927
|
}
|
|
940
928
|
|
|
941
|
-
function hyphenated(node:
|
|
929
|
+
function hyphenated(node: AstNode): Type | undefined {
|
|
942
930
|
if (
|
|
943
931
|
node.type !== 'BinaryExpression' ||
|
|
944
932
|
node.operator !== '-' ||
|
|
945
933
|
node.left.type !== 'Identifier' ||
|
|
946
|
-
node.right
|
|
947
|
-
|
|
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)))
|
|
948
944
|
) {
|
|
949
945
|
return undefined
|
|
950
946
|
}
|
|
951
|
-
const left = node.left as Identifier
|
|
952
947
|
const reported = diagnostics.length
|
|
953
948
|
const subject = left.from ? infer(left.from) : root
|
|
954
949
|
diagnostics.length = reported
|
|
955
|
-
if (subject.kind !== 'record' || subject.fields.has(
|
|
950
|
+
if (subject.kind !== 'record' || subject.fields.has(name)) {
|
|
956
951
|
return undefined
|
|
957
952
|
}
|
|
958
|
-
const whole = `${
|
|
953
|
+
const whole = `${name}-${node.right.value}`
|
|
959
954
|
const found = subject.fields.get(whole)
|
|
960
955
|
if (!found) {
|
|
961
956
|
return undefined
|
|
962
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 })
|
|
963
962
|
report(
|
|
964
963
|
'unknown-field',
|
|
965
964
|
'warning',
|
|
966
965
|
node,
|
|
967
966
|
`${print(node)} subtracts; ${whole} is one field`,
|
|
968
|
-
[text(
|
|
967
|
+
holder ? [text(holder, whole)] : []
|
|
969
968
|
)
|
|
970
969
|
return found
|
|
971
970
|
}
|
|
@@ -1048,11 +1047,9 @@ export function check(
|
|
|
1048
1047
|
}
|
|
1049
1048
|
}
|
|
1050
1049
|
|
|
1051
|
-
function binary(
|
|
1052
|
-
node: Extract<AstNodeUnion, { type: 'BinaryExpression' }>
|
|
1053
|
-
): Type {
|
|
1050
|
+
function binary(node: BinaryExpression): Type {
|
|
1054
1051
|
const { operator } = node
|
|
1055
|
-
const rightNode = node.right
|
|
1052
|
+
const rightNode = node.right
|
|
1056
1053
|
if (operator === '&&' || operator === '||' || operator === '??') {
|
|
1057
1054
|
const left = infer(node.left)
|
|
1058
1055
|
const right = infer(rightNode)
|
|
@@ -1147,7 +1144,7 @@ export function check(
|
|
|
1147
1144
|
if (EQUALITY.has(operator)) {
|
|
1148
1145
|
const leftLiteral = literalOf(leftItem)
|
|
1149
1146
|
const rightLiteral = literalOf(rightItem)
|
|
1150
|
-
if (rightLiteral !== undefined && node.right
|
|
1147
|
+
if (rightLiteral !== undefined && node.right.type === 'Literal') {
|
|
1151
1148
|
compareLiteral(leftItem, rightLiteral, node)
|
|
1152
1149
|
} else if (leftLiteral !== undefined && node.left.type === 'Literal') {
|
|
1153
1150
|
compareLiteral(rightItem, leftLiteral, node)
|
|
@@ -1234,7 +1231,7 @@ export function check(
|
|
|
1234
1231
|
type: 'Identifier',
|
|
1235
1232
|
value: name,
|
|
1236
1233
|
from: subjectNode
|
|
1237
|
-
}
|
|
1234
|
+
}
|
|
1238
1235
|
}
|
|
1239
1236
|
for (const name of prefix) {
|
|
1240
1237
|
step(String(name), node)
|
|
@@ -1261,9 +1258,7 @@ export function check(
|
|
|
1261
1258
|
node,
|
|
1262
1259
|
`no function ${name}` +
|
|
1263
1260
|
(near.length ? `; did you mean ${near.join(', ')}?` : ''),
|
|
1264
|
-
near.map((candidate) =>
|
|
1265
|
-
print({ ...node, name: candidate } as FunctionCall)
|
|
1266
|
-
)
|
|
1261
|
+
near.map((candidate) => print({ ...node, name: candidate }))
|
|
1267
1262
|
)
|
|
1268
1263
|
}
|
|
1269
1264
|
return UNKNOWN
|
|
@@ -1290,10 +1285,10 @@ export function check(
|
|
|
1290
1285
|
}
|
|
1291
1286
|
const accepted: readonly ParamType[] =
|
|
1292
1287
|
typeof declared === 'string' ? [declared] : declared
|
|
1293
|
-
if (
|
|
1288
|
+
if (arg.type === 'Lambda') {
|
|
1294
1289
|
const item =
|
|
1295
1290
|
lastCollection?.kind === 'list' ? lastCollection.of : UNKNOWN
|
|
1296
|
-
types.push(lambda(arg
|
|
1291
|
+
types.push(lambda(arg, lambdaParams(name, item)))
|
|
1297
1292
|
continue
|
|
1298
1293
|
}
|
|
1299
1294
|
const type = infer(arg)
|
|
@@ -1341,8 +1336,7 @@ export function check(
|
|
|
1341
1336
|
return returns ?? UNKNOWN
|
|
1342
1337
|
}
|
|
1343
1338
|
|
|
1344
|
-
function infer(
|
|
1345
|
-
const node = ast as Node
|
|
1339
|
+
function infer(node: AstNode): Type {
|
|
1346
1340
|
switch (node.type) {
|
|
1347
1341
|
case 'Literal': {
|
|
1348
1342
|
const { value } = node
|
|
@@ -1403,7 +1397,7 @@ export function check(
|
|
|
1403
1397
|
return binary(node)
|
|
1404
1398
|
}
|
|
1405
1399
|
case 'UnaryExpression': {
|
|
1406
|
-
const right = infer(node.right
|
|
1400
|
+
const right = infer(node.right)
|
|
1407
1401
|
if (node.operator === '!') {
|
|
1408
1402
|
return BOOLEAN
|
|
1409
1403
|
}
|
|
@@ -1468,7 +1462,7 @@ export function check(
|
|
|
1468
1462
|
return last
|
|
1469
1463
|
}
|
|
1470
1464
|
case 'AssignmentExpression': {
|
|
1471
|
-
const value = infer(node.right
|
|
1465
|
+
const value = infer(node.right)
|
|
1472
1466
|
scope.set(node.left.value, value)
|
|
1473
1467
|
return value
|
|
1474
1468
|
}
|
|
@@ -1476,7 +1470,7 @@ export function check(
|
|
|
1476
1470
|
return lambda(node)
|
|
1477
1471
|
}
|
|
1478
1472
|
default: {
|
|
1479
|
-
|
|
1473
|
+
return unknownNode(node)
|
|
1480
1474
|
}
|
|
1481
1475
|
}
|
|
1482
1476
|
}
|
package/src/collections.ts
CHANGED
|
@@ -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'
|
|
6
|
+
import { analyze, literalValue } from './analyze.ts'
|
|
7
7
|
import { print } from './print.ts'
|
|
8
8
|
|
|
9
9
|
import type { PathKey } from './analyze.ts'
|
|
10
|
-
import type { AstNode
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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(
|
|
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
|
|
77
|
-
return
|
|
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
|
-
|
|
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
|
|
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 =
|
|
133
|
-
if (subject && value) {
|
|
134
|
-
return { subject, op: operator as '==', 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 =
|
|
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 =
|
|
149
|
-
return subject && typeof value
|
|
150
|
-
? { subject, op: operator, 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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 =
|
|
141
|
+
const key = literalValue(left)
|
|
163
142
|
const container = subjectOf(right, opts)
|
|
164
|
-
return container && typeof key
|
|
165
|
-
? { subject: container, op: 'has', value: key
|
|
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
|
-
|
|
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
|
|
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 }
|
|
177
|
+
let node: AstNode = { type: 'Identifier', value: row }
|
|
201
178
|
for (const key of path) {
|
|
202
179
|
node =
|
|
203
180
|
typeof key === 'string'
|
|
204
|
-
?
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
+
}
|
package/src/evaluator/compile.ts
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
* Copyright 2020 Tom Shawver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { LAMBDA } from '../collections.ts'
|
|
7
|
+
import { unknownNode } from '../errors.ts'
|
|
6
8
|
import { and, nullish, or } from '../grammar.ts'
|
|
7
9
|
|
|
8
10
|
import type { Grammar } from '../grammar.ts'
|
|
9
|
-
import type {
|
|
10
|
-
AstNode,
|
|
11
|
-
AstNodeUnion,
|
|
12
|
-
JexlFunction,
|
|
13
|
-
JexlValue,
|
|
14
|
-
Literal
|
|
15
|
-
} from '../types.ts'
|
|
11
|
+
import type { AstNode, JexlFunction, JexlValue } from '../types.ts'
|
|
16
12
|
|
|
17
13
|
/** The variables an expression is evaluated against. */
|
|
18
14
|
export type Context = Record<string, JexlValue>
|
|
@@ -44,6 +40,16 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
|
44
40
|
target[key] = value
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
// inherited, these lead from any value to `Function` and `Object.prototype`
|
|
44
|
+
const isUnsafeKey = (key: string | number) =>
|
|
45
|
+
key === 'constructor' || key === '__proto__'
|
|
46
|
+
|
|
47
|
+
function member(target: Context, key: string | number) {
|
|
48
|
+
return isUnsafeKey(key) && !Object.hasOwn(target, key)
|
|
49
|
+
? undefined
|
|
50
|
+
: target[key]
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/** The key `subject[index]` reads under, or undefined when it reads nothing. */
|
|
48
54
|
function memberKey(index: JexlValue) {
|
|
49
55
|
return typeof index === 'string' || typeof index === 'number'
|
|
@@ -90,23 +96,27 @@ function isKeyPart(
|
|
|
90
96
|
* serves every evaluation.
|
|
91
97
|
*/
|
|
92
98
|
function literalTable(node: AstNode) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
? Object.freeze(literal.value.map((item) => (item as Literal).value))
|
|
99
|
+
if (node.type === 'ArrayLiteral') {
|
|
100
|
+
return node.value.every((item) => item.type === 'Literal')
|
|
101
|
+
? Object.freeze(node.value.map((item) => item.value))
|
|
97
102
|
: undefined
|
|
98
103
|
}
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return Object.freeze(table)
|
|
104
|
+
if (node.type !== 'ObjectLiteral') {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
const table: Context = {}
|
|
108
|
+
for (const [key, value] of Object.entries(node.value)) {
|
|
109
|
+
if (value.type !== 'Literal') {
|
|
110
|
+
return undefined
|
|
107
111
|
}
|
|
112
|
+
defineOwn(table, key, value.value)
|
|
108
113
|
}
|
|
109
|
-
return
|
|
114
|
+
return Object.freeze(table)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function lambda(fn: JexlFunction & { [LAMBDA]?: true }) {
|
|
118
|
+
fn[LAMBDA] = true
|
|
119
|
+
return fn
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
const unassigned = Symbol('unassigned')
|
|
@@ -208,8 +218,7 @@ function climb(hops: number, index: number): CompiledNode {
|
|
|
208
218
|
}
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
function assignedNames(
|
|
212
|
-
const node = ast as AstNodeUnion | undefined
|
|
221
|
+
function assignedNames(node: AstNode | undefined, names = new Set<string>()) {
|
|
213
222
|
switch (node?.type) {
|
|
214
223
|
case 'AssignmentExpression': {
|
|
215
224
|
names.add(node.left.value)
|
|
@@ -335,15 +344,11 @@ function stringify(value: JexlValue) {
|
|
|
335
344
|
}
|
|
336
345
|
|
|
337
346
|
function compileNode(
|
|
338
|
-
|
|
347
|
+
node: AstNode,
|
|
339
348
|
grammar: Grammar,
|
|
340
349
|
scope: Scope
|
|
341
350
|
): CompiledNode {
|
|
342
351
|
const compile = (child: AstNode) => compileNode(child, grammar, scope)
|
|
343
|
-
// AstNode types its `type` as a plain string so the Parser can build the tree
|
|
344
|
-
// loosely; narrowing to the union once, here, lets every case below see its
|
|
345
|
-
// own node type instead of repeating the same cast in each branch
|
|
346
|
-
const node = ast as AstNodeUnion
|
|
347
352
|
switch (node.type) {
|
|
348
353
|
case 'Literal': {
|
|
349
354
|
const { value } = node
|
|
@@ -365,6 +370,12 @@ function compileNode(
|
|
|
365
370
|
: (getMember(target, name) as JexlValue)
|
|
366
371
|
}
|
|
367
372
|
}
|
|
373
|
+
if (isUnsafeKey(name)) {
|
|
374
|
+
return (ctx) => {
|
|
375
|
+
const target = from(ctx)
|
|
376
|
+
return target == null ? undefined : member(target as Context, name)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
368
379
|
return (ctx) => {
|
|
369
380
|
const target = from(ctx)
|
|
370
381
|
return target == null ? undefined : (target as Context)[name]
|
|
@@ -374,7 +385,7 @@ function compileNode(
|
|
|
374
385
|
case 'BinaryExpression': {
|
|
375
386
|
const op = grammar.elements[node.operator]
|
|
376
387
|
const left = compile(node.left)
|
|
377
|
-
const right = compile(node.right
|
|
388
|
+
const right = compile(node.right)
|
|
378
389
|
if (op?.type === 'binaryOp' && op.evalOnDemand) {
|
|
379
390
|
const { evalOnDemand } = op
|
|
380
391
|
if (evalOnDemand === and) {
|
|
@@ -405,7 +416,7 @@ function compileNode(
|
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
case 'UnaryExpression': {
|
|
408
|
-
const right = compile(node.right
|
|
419
|
+
const right = compile(node.right)
|
|
409
420
|
const elem = grammar.elements[node.operator]
|
|
410
421
|
const fn =
|
|
411
422
|
elem?.type === 'unaryOp'
|
|
@@ -452,7 +463,7 @@ function compileNode(
|
|
|
452
463
|
const key = memberKey(index(ctx))
|
|
453
464
|
return subjectVal == null || key === undefined
|
|
454
465
|
? undefined
|
|
455
|
-
: (subjectVal as
|
|
466
|
+
: member(subjectVal as Context, key)
|
|
456
467
|
}
|
|
457
468
|
}
|
|
458
469
|
|
|
@@ -568,7 +579,7 @@ function compileNode(
|
|
|
568
579
|
if (scope.params) {
|
|
569
580
|
throw new Error('Assignment is not supported in a lambda')
|
|
570
581
|
}
|
|
571
|
-
const right = compile(node.right
|
|
582
|
+
const right = compile(node.right)
|
|
572
583
|
const { slots, frame } = scope.locals!
|
|
573
584
|
const slot = slots.get(node.left.value)!
|
|
574
585
|
return (ctx) => (frame.locals[slot] = right(ctx))
|
|
@@ -581,23 +592,25 @@ function compileNode(
|
|
|
581
592
|
})
|
|
582
593
|
if (!scope.params) {
|
|
583
594
|
const frame = scope.locals?.frame
|
|
584
|
-
return (ctx)
|
|
595
|
+
return (ctx) => {
|
|
585
596
|
const locals = frame?.locals
|
|
586
|
-
return (...args) =>
|
|
597
|
+
return lambda((...args) =>
|
|
587
598
|
body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
|
|
599
|
+
)
|
|
588
600
|
}
|
|
589
601
|
}
|
|
590
|
-
return (env)
|
|
602
|
+
return (env) => {
|
|
591
603
|
const outer = asFrame(env)
|
|
592
|
-
return (...args) =>
|
|
604
|
+
return lambda((...args) =>
|
|
593
605
|
body(
|
|
594
606
|
asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
|
|
595
607
|
)
|
|
608
|
+
)
|
|
596
609
|
}
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
default: {
|
|
600
|
-
|
|
613
|
+
return unknownNode(node)
|
|
601
614
|
}
|
|
602
615
|
}
|
|
603
616
|
}
|