@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.
- package/README.md +8 -3
- package/dist/Expression.d.ts +0 -12
- package/dist/Expression.js +0 -16
- package/dist/Expression.js.map +1 -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.d.ts +0 -5
- package/dist/check.js +62 -240
- 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 +28 -50
- 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/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/index.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 +9 -0
- package/dist/print.js +183 -0
- package/dist/print.js.map +1 -0
- package/dist/types.d.ts +18 -21
- package/esm/Expression.d.ts +0 -12
- package/esm/Expression.js +0 -16
- package/esm/Expression.js.map +1 -1
- 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.d.ts +0 -5
- package/esm/check.js +29 -206
- 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 +27 -49
- 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/index.d.ts +2 -1
- package/esm/index.js +2 -1
- package/esm/index.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 +9 -0
- package/esm/print.js +177 -0
- package/esm/print.js.map +1 -0
- package/esm/types.d.ts +18 -21
- package/package.json +2 -1
- package/src/Expression.ts +0 -20
- package/src/Jexl.ts +13 -20
- package/src/Lexer.ts +42 -144
- package/src/analyze.ts +10 -10
- package/src/check.ts +48 -250
- package/src/collections.ts +11 -1
- package/src/conditions.ts +39 -62
- package/src/errors.ts +10 -0
- package/src/evaluator/compile.ts +49 -36
- package/src/grammar.ts +3 -9
- package/src/index.ts +2 -1
- package/src/operators.ts +10 -6
- package/src/parser/Parser.ts +13 -19
- package/src/print.ts +203 -0
- 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
|
-
|
|
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
|
-
|
|
781
|
-
?
|
|
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
|
|
834
|
+
function dottedKey(node: Identifier): Type | undefined {
|
|
1036
835
|
const names = [node.value]
|
|
1037
|
-
let base
|
|
1038
|
-
while (base.
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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:
|
|
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
|
|
1143
|
-
|
|
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(
|
|
950
|
+
if (subject.kind !== 'record' || subject.fields.has(name)) {
|
|
1152
951
|
return undefined
|
|
1153
952
|
}
|
|
1154
|
-
const whole = `${
|
|
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(
|
|
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
|
|
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
|
-
}
|
|
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 (
|
|
1288
|
+
if (arg.type === 'Lambda') {
|
|
1490
1289
|
const item =
|
|
1491
1290
|
lastCollection?.kind === 'list' ? lastCollection.of : UNKNOWN
|
|
1492
|
-
types.push(lambda(arg
|
|
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(
|
|
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
|
-
|
|
1473
|
+
return unknownNode(node)
|
|
1676
1474
|
}
|
|
1677
1475
|
}
|
|
1678
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'
|
|
7
|
-
import { print } from './
|
|
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
|
|
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
|
+
}
|