@jbrowse/jexl 3.1.0 → 3.3.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 (54) hide show
  1. package/README.md +15 -0
  2. package/dist/Expression.d.ts +3 -1
  3. package/dist/Expression.js +3 -8
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.d.ts +6 -6
  6. package/dist/Jexl.js +7 -1
  7. package/dist/Jexl.js.map +1 -1
  8. package/dist/Lexer.d.ts +0 -1
  9. package/dist/Lexer.js +5 -8
  10. package/dist/Lexer.js.map +1 -1
  11. package/dist/evaluator/compile.js +34 -16
  12. package/dist/evaluator/compile.js.map +1 -1
  13. package/dist/grammar.d.ts +19 -2
  14. package/dist/grammar.js +8 -8
  15. package/dist/grammar.js.map +1 -1
  16. package/dist/parser/Parser.d.ts +2 -1
  17. package/dist/parser/Parser.js +29 -12
  18. package/dist/parser/Parser.js.map +1 -1
  19. package/dist/parser/handlers.d.ts +8 -2
  20. package/dist/parser/handlers.js +32 -7
  21. package/dist/parser/handlers.js.map +1 -1
  22. package/dist/parser/states.js +19 -20
  23. package/dist/parser/states.js.map +1 -1
  24. package/esm/Expression.d.ts +3 -1
  25. package/esm/Expression.js +3 -8
  26. package/esm/Expression.js.map +1 -1
  27. package/esm/Jexl.d.ts +6 -6
  28. package/esm/Jexl.js +7 -1
  29. package/esm/Jexl.js.map +1 -1
  30. package/esm/Lexer.d.ts +0 -1
  31. package/esm/Lexer.js +5 -8
  32. package/esm/Lexer.js.map +1 -1
  33. package/esm/evaluator/compile.js +34 -16
  34. package/esm/evaluator/compile.js.map +1 -1
  35. package/esm/grammar.d.ts +19 -2
  36. package/esm/grammar.js +8 -8
  37. package/esm/grammar.js.map +1 -1
  38. package/esm/parser/Parser.d.ts +2 -1
  39. package/esm/parser/Parser.js +29 -12
  40. package/esm/parser/Parser.js.map +1 -1
  41. package/esm/parser/handlers.d.ts +8 -2
  42. package/esm/parser/handlers.js +29 -7
  43. package/esm/parser/handlers.js.map +1 -1
  44. package/esm/parser/states.js +19 -20
  45. package/esm/parser/states.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/Expression.ts +3 -9
  48. package/src/Jexl.ts +18 -9
  49. package/src/Lexer.ts +5 -8
  50. package/src/evaluator/compile.ts +51 -44
  51. package/src/grammar.ts +30 -11
  52. package/src/parser/Parser.ts +35 -17
  53. package/src/parser/handlers.ts +33 -10
  54. package/src/parser/states.ts +20 -20
@@ -4,22 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { Grammar } from '../grammar.ts'
7
- import type {
8
- ArrayLiteral,
9
- AssignmentExpression,
10
- AstNode,
11
- BinaryExpression,
12
- ConditionalExpression,
13
- FilterExpression,
14
- FunctionCall,
15
- Identifier,
16
- JexlValue,
17
- Literal,
18
- ObjectLiteral,
19
- SequenceExpression,
20
- TemplateLiteral,
21
- UnaryExpression
22
- } from '../types.ts'
7
+ import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
23
8
  import type Evaluator from './Evaluator.ts'
24
9
 
25
10
  /**
@@ -28,6 +13,34 @@ import type Evaluator from './Evaluator.ts'
28
13
  */
29
14
  export type CompiledNode = (ev: Evaluator) => JexlValue
30
15
 
16
+ /**
17
+ * Writes a key that a plain assignment would mishandle. Storing to
18
+ * "__proto__" invokes the prototype setter rather than creating a property, so
19
+ * an object literal or assignment using that key silently lost its value (and
20
+ * re-pointed the target's prototype). Defining the property instead matches
21
+ * what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
22
+ */
23
+ function defineOwn(
24
+ target: Record<string, JexlValue>,
25
+ key: string,
26
+ value: JexlValue
27
+ ) {
28
+ Object.defineProperty(target, key, {
29
+ value,
30
+ writable: true,
31
+ enumerable: true,
32
+ configurable: true
33
+ })
34
+ }
35
+
36
+ function assignOwn(
37
+ target: Record<string, JexlValue>,
38
+ key: string,
39
+ value: JexlValue
40
+ ) {
41
+ target[key] = value
42
+ }
43
+
31
44
  /** Renders an interpolated value for a template literal. */
32
45
  function stringify(value: JexlValue) {
33
46
  if (value == null) {
@@ -62,14 +75,17 @@ function stringify(value: JexlValue) {
62
75
  * @throws {Error} if the tree contains an unrecognized node type
63
76
  */
64
77
  export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
65
- switch (ast.type) {
78
+ // AstNode types its `type` as a plain string so the Parser can build the tree
79
+ // loosely; narrowing to the union once, here, lets every case below see its
80
+ // own node type instead of repeating the same cast in each branch
81
+ const node = ast as AstNodeUnion
82
+ switch (node.type) {
66
83
  case 'Literal': {
67
- const { value } = ast as Literal
84
+ const { value } = node
68
85
  return () => value
69
86
  }
70
87
 
71
88
  case 'Identifier': {
72
- const node = ast as Identifier
73
89
  const name = node.value
74
90
  if (!node.from) {
75
91
  return node.relative
@@ -91,7 +107,6 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
91
107
  }
92
108
 
93
109
  case 'BinaryExpression': {
94
- const node = ast as BinaryExpression
95
110
  const op = grammar.elements[node.operator]
96
111
  if (op?.type !== 'binaryOp') {
97
112
  // matches the tree-walking behaviour: an unknown operator yields
@@ -119,7 +134,6 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
119
134
  }
120
135
 
121
136
  case 'UnaryExpression': {
122
- const node = ast as UnaryExpression
123
137
  const right = compileAst(node.right!, grammar)
124
138
  const elem = grammar.elements[node.operator]
125
139
  const fn =
@@ -139,7 +153,6 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
139
153
  }
140
154
 
141
155
  case 'ConditionalExpression': {
142
- const node = ast as ConditionalExpression
143
156
  const test = compileAst(node.test, grammar)
144
157
  const consequent = node.consequent
145
158
  ? compileAst(node.consequent, grammar)
@@ -158,7 +171,6 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
158
171
  }
159
172
 
160
173
  case 'FilterExpression': {
161
- const node = ast as FilterExpression
162
174
  if (node.relative) {
163
175
  // thrown on evaluation rather than compilation, as before
164
176
  return () => {
@@ -180,9 +192,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
180
192
  }
181
193
 
182
194
  case 'ArrayLiteral': {
183
- const items = (ast as ArrayLiteral).value.map((item) =>
184
- compileAst(item, grammar)
185
- )
195
+ const items = node.value.map((item) => compileAst(item, grammar))
186
196
  const len = items.length
187
197
  return (ev) => {
188
198
  const out: JexlValue[] = new Array(len)
@@ -194,14 +204,16 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
194
204
  }
195
205
 
196
206
  case 'ObjectLiteral': {
197
- const entries = Object.entries((ast as ObjectLiteral).value)
207
+ const entries = Object.entries(node.value)
198
208
  const keys = entries.map(([key]) => key)
199
- const values = entries.map(([, node]) => compileAst(node, grammar))
209
+ const values = entries.map(([, value]) => compileAst(value, grammar))
200
210
  const len = keys.length
211
+ // resolved once, here, so the common case keeps its plain store
212
+ const store = keys.includes('__proto__') ? defineOwn : assignOwn
201
213
  return (ev) => {
202
214
  const out: Record<string, JexlValue> = {}
203
215
  for (let i = 0; i < len; i++) {
204
- out[keys[i]!] = values[i]!(ev)
216
+ store(out, keys[i]!, values[i]!(ev))
205
217
  }
206
218
  return out
207
219
  }
@@ -209,16 +221,14 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
209
221
 
210
222
  case 'TemplateLiteral': {
211
223
  // every part renders to a string, so these are narrower than CompiledNode
212
- const parts = (ast as TemplateLiteral).parts.map(
213
- (part): ((ev: Evaluator) => string) => {
214
- if (part.type === 'static') {
215
- const { value } = part
216
- return () => value
217
- }
218
- const expr = compileAst(part.value, grammar)
219
- return (ev) => stringify(expr(ev))
224
+ const parts = node.parts.map((part): ((ev: Evaluator) => string) => {
225
+ if (part.type === 'static') {
226
+ const { value } = part
227
+ return () => value
220
228
  }
221
- )
229
+ const expr = compileAst(part.value, grammar)
230
+ return (ev) => stringify(expr(ev))
231
+ })
222
232
  return (ev) => {
223
233
  let out = ''
224
234
  for (const part of parts) {
@@ -229,7 +239,6 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
229
239
  }
230
240
 
231
241
  case 'FunctionCall': {
232
- const node = ast as FunctionCall
233
242
  const { name } = node
234
243
  const args = node.args.map((arg) => compileAst(arg, grammar))
235
244
  // hasOwn, so that inherited Object.prototype members such as `toString`
@@ -279,9 +288,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
279
288
  }
280
289
 
281
290
  case 'SequenceExpression': {
282
- const exprs = (ast as SequenceExpression).expressions.map((expr) =>
283
- compileAst(expr, grammar)
284
- )
291
+ const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
285
292
  const len = exprs.length
286
293
  return (ev) => {
287
294
  let last: JexlValue
@@ -293,12 +300,12 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
293
300
  }
294
301
 
295
302
  case 'AssignmentExpression': {
296
- const node = ast as AssignmentExpression
297
303
  const name = node.left.value
298
304
  const right = compileAst(node.right!, grammar)
305
+ const store = name === '__proto__' ? defineOwn : assignOwn
299
306
  return (ev) => {
300
307
  const value = right(ev)
301
- ev._context[name] = value
308
+ store(ev._context, name, value)
302
309
  return value
303
310
  }
304
311
  }
package/src/grammar.ts CHANGED
@@ -9,6 +9,21 @@ import type { JexlValue } from './types.ts'
9
9
 
10
10
  export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
11
11
 
12
+ /**
13
+ * A function or operator implementation as its author writes it, for the
14
+ * registration methods on {@link Jexl} to accept.
15
+ *
16
+ * Jexl cannot check what a registered callback will be handed. The arguments
17
+ * are whatever the expression evaluated to, drawn from a context supplied at
18
+ * evaluation time, and that context routinely holds values jexl has no literal
19
+ * for — a host object with methods, a callback, a class instance. Requiring
20
+ * {@link JexlValue} parameters therefore rejected every callback actually
21
+ * written against such a value, since parameters are contravariant. The
22
+ * argument types are the caller's business; jexl only promises to pass along
23
+ * whatever the operands evaluated to.
24
+ */
25
+ export type UncheckedFn = (...args: never[]) => unknown
26
+
12
27
  export type BinaryOpEvalOnDemand = (
13
28
  left: { eval: () => JexlValue },
14
29
  right: { eval: () => JexlValue }
@@ -27,10 +42,12 @@ export interface BinaryOp {
27
42
  unaryEval?: (right: JexlValue) => JexlValue
28
43
  }
29
44
 
45
+ export type UnaryOpEval = (right: JexlValue) => JexlValue
46
+
30
47
  export interface UnaryOp {
31
48
  type: 'unaryOp'
32
49
  precedence: number
33
- eval: (right: JexlValue) => JexlValue
50
+ eval: UnaryOpEval
34
51
  }
35
52
 
36
53
  /**
@@ -56,9 +73,12 @@ export interface SimpleElement {
56
73
 
57
74
  export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
58
75
 
76
+ /** A registered function, as jexl calls it once the operands are evaluated. */
77
+ export type GrammarFn = (...args: JexlValue[]) => JexlValue
78
+
59
79
  export interface Grammar {
60
80
  elements: Record<string, GrammarElement>
61
- functions: Record<string, (...args: JexlValue[]) => JexlValue>
81
+ functions: Record<string, GrammarFn>
62
82
  }
63
83
 
64
84
  /**
@@ -182,18 +202,17 @@ export const getGrammar = (): Grammar => ({
182
202
  precedence: 20,
183
203
  eval: (left, right) => {
184
204
  if (typeof right === 'string') {
185
- const leftStr =
186
- typeof left === 'string' ||
205
+ // only a primitive has a meaningful substring form. An absent or
206
+ // structured left operand is not "in" a string, and must not be
207
+ // coerced to '' — every string contains the empty string, so that
208
+ // made `feature.missingAttr in "someString"` true for every feature.
209
+ return typeof left === 'string' ||
187
210
  typeof left === 'number' ||
188
211
  typeof left === 'boolean'
189
- ? String(left)
190
- : ''
191
- return right.includes(leftStr)
192
- }
193
- if (Array.isArray(right)) {
194
- return right.includes(left)
212
+ ? right.includes(String(left))
213
+ : false
195
214
  }
196
- return false
215
+ return Array.isArray(right) ? right.includes(left) : false
197
216
  }
198
217
  },
199
218
  '!': {
@@ -10,6 +10,25 @@ import type Lexer from '../Lexer.ts'
10
10
  import type { Grammar } from '../grammar.ts'
11
11
  import type { AstNode, SequenceExpression, Token } from '../types.ts'
12
12
 
13
+ /**
14
+ * The handler to run for a token type when the state it appears in doesn't
15
+ * name one of its own. Token types absent here — the brackets, parens and
16
+ * separators — carry no behavior beyond the state transition the state machine
17
+ * gives them.
18
+ */
19
+ const tokenHandlers: Record<
20
+ string,
21
+ ((this: Parser, token: Token) => void) | undefined
22
+ > = {
23
+ binaryOp: handlers.binaryOp,
24
+ dot: handlers.dot,
25
+ identifier: handlers.identifier,
26
+ literal: handlers.literal,
27
+ semicolon: handlers.semicolon,
28
+ templateString: handlers.templateString,
29
+ unaryOp: handlers.unaryOp
30
+ }
31
+
13
32
  /**
14
33
  * The Parser is a state machine that converts tokens from the {@link Lexer}
15
34
  * into an Abstract Syntax Tree (AST), capable of being evaluated in any
@@ -76,10 +95,8 @@ class Parser {
76
95
  const startExpr = this._exprStr
77
96
  this._exprStr += token.raw
78
97
  if (state.subHandler) {
79
- if (!this._subParser) {
80
- this._startSubExpression(startExpr)
81
- }
82
- const stopState = this._subParser!.addToken(token)
98
+ const subParser = this._subParser ?? this._startSubExpression(startExpr)
99
+ const stopState = subParser.addToken(token)
83
100
  if (stopState) {
84
101
  this._endSubExpression()
85
102
  if (stopState === '_semicolon') {
@@ -94,18 +111,10 @@ class Parser {
94
111
  return this._stopMap[token.type]!
95
112
  } else if (state.tokenTypes?.[token.type]) {
96
113
  const typeOpts = state.tokenTypes[token.type]!
97
- let handleFunc = (
98
- handlers as Record<
99
- string,
100
- ((this: Parser, token: Token) => void) | undefined
101
- >
102
- )[token.type]
103
- if (typeOpts.handler) {
104
- handleFunc = typeOpts.handler
105
- }
106
- if (handleFunc) {
107
- handleFunc.call(this, token)
108
- }
114
+ // the state's own handler wins over the token type's default one, and a
115
+ // token type with neither only drives the transition below
116
+ const handleFunc = typeOpts.handler ?? tokenHandlers[token.type]
117
+ handleFunc?.call(this, token)
109
118
  if (typeOpts.toState) {
110
119
  this._state = typeOpts.toState
111
120
  }
@@ -140,7 +149,14 @@ class Parser {
140
149
  * the expression, indicating that the expression is incomplete
141
150
  */
142
151
  complete() {
143
- if (this._cursor && !states[this._state]!.completable) {
152
+ // `_subParser` counts as unfinished business alongside `_cursor`: a group
153
+ // that opened before anything was placed in the tree, as in "(1", leaves
154
+ // the cursor null, and testing the cursor alone let the missing ")" pass
155
+ // silently and evaluate as if it were closed.
156
+ if (
157
+ (this._cursor || this._subParser) &&
158
+ !states[this._state]!.completable
159
+ ) {
144
160
  throw new Error(`Unexpected end of expression: ${this._exprStr}`)
145
161
  }
146
162
  if (this._subParser) {
@@ -231,6 +247,7 @@ class Parser {
231
247
  * Prepares the Parser to accept a subexpression by (re)instantiating the
232
248
  * subParser.
233
249
  * @param {string} [exprStr] The expression string to prefix to the new Parser
250
+ * @returns {Parser} the new subParser, also stored on this instance
234
251
  * @private
235
252
  */
236
253
  _startSubExpression(exprStr?: string) {
@@ -243,6 +260,7 @@ class Parser {
243
260
  endStates = { ...endStates, semicolon: '_semicolon' }
244
261
  }
245
262
  this._subParser = new Parser(this._grammar, this._lexer, exprStr, endStates)
263
+ return this._subParser
246
264
  }
247
265
  }
248
266
 
@@ -4,6 +4,9 @@
4
4
  */
5
5
 
6
6
  import { precedenceOf } from '../grammar.ts'
7
+ // circular with Parser.ts, which is fine: the binding is only read from inside
8
+ // a handler, long after both modules have finished evaluating
9
+ import Parser from './Parser.ts'
7
10
 
8
11
  import type {
9
12
  ArrayLiteral,
@@ -20,7 +23,6 @@ import type {
20
23
  Token,
21
24
  UnaryExpression
22
25
  } from '../types.ts'
23
- import type Parser from './Parser.ts'
24
26
 
25
27
  /**
26
28
  * Handles a subexpression that's used to define a transform argument's value.
@@ -145,13 +147,26 @@ export function filter(this: Parser, ast: AstNode | null) {
145
147
  /**
146
148
  * Handles identifier tokens when used to indicate the name of a function to
147
149
  * be called.
148
- * @param {{type: <string>}} token A token object
150
+ *
151
+ * A call written against a value, `a.b(x)`, becomes `b(a, x)`: jexl functions
152
+ * live in one global pool and have no receiver, so the value the name was read
153
+ * from is passed as the first argument instead. Every function in the pool is
154
+ * already written that way — `get(feature, key)`, `split(str, sep)` — which is
155
+ * what makes the two spellings equivalent. Previously the receiver was simply
156
+ * dropped, so `refName.split(' ')` quietly called `split(' ')`.
149
157
  */
150
158
  export function functionCall(this: Parser) {
159
+ const cursor = this._cursor
160
+ if (cursor?.type !== 'Identifier') {
161
+ // reached by `a[expr]()`, whose subject is a filter rather than a name;
162
+ // there is nothing to look up in the function pool
163
+ throw new Error(`Functions must be called by name: ${this._exprStr}`)
164
+ }
165
+ const { from } = cursor as Identifier
151
166
  const node: FunctionCall = {
152
167
  type: 'FunctionCall',
153
- name: (this._cursor as Identifier).value,
154
- args: [],
168
+ name: (cursor as Identifier).value,
169
+ args: from ? [from] : [],
155
170
  pool: 'functions'
156
171
  }
157
172
  this._placeBeforeCursor(node)
@@ -209,11 +224,7 @@ export function templateString(this: Parser, token: Token) {
209
224
  })
210
225
  } else {
211
226
  const subTokens = this._lexer.tokenize(part.value)
212
- const ParserClass = this.constructor as new (
213
- grammar: typeof this._grammar,
214
- lexer: typeof this._lexer
215
- ) => Parser
216
- const subParser = new ParserClass(this._grammar, this._lexer)
227
+ const subParser = new Parser(this._grammar, this._lexer)
217
228
  subParser.addTokens(subTokens)
218
229
  const subAst = subParser.complete()
219
230
 
@@ -261,7 +272,19 @@ export function objStart(this: Parser) {
261
272
  * @param {{type: <string>}} ast The subexpression tree
262
273
  */
263
274
  export function objVal(this: Parser, ast: AstNode | null) {
264
- ;(this._cursor as ObjectLiteral).value[this._curObjKey!] = ast!
275
+ // defined rather than assigned so that the key "__proto__" becomes an
276
+ // ordinary entry of the node's key map instead of re-pointing its prototype,
277
+ // which dropped the entry before the Evaluator ever saw it
278
+ Object.defineProperty(
279
+ (this._cursor as ObjectLiteral).value,
280
+ this._curObjKey!,
281
+ {
282
+ value: ast!,
283
+ writable: true,
284
+ enumerable: true,
285
+ configurable: true
286
+ }
287
+ )
265
288
  }
266
289
 
267
290
  /**
@@ -22,6 +22,20 @@ interface StateDefinition {
22
22
 
23
23
  type States = Record<string, StateDefinition>
24
24
 
25
+ /**
26
+ * The tokens that may follow a finished operand: an operator to combine it
27
+ * with, a dot or subscript to keep drilling into it, a question mark opening a
28
+ * ternary, or a semicolon ending the statement. Shared by every state that
29
+ * sits on a complete value, which is also why each of them is completable.
30
+ */
31
+ const afterOperand: Record<string, TokenTypeOptions> = {
32
+ binaryOp: { toState: 'expectOperand' },
33
+ dot: { toState: 'traverse' },
34
+ openBracket: { toState: 'filter' },
35
+ question: { toState: 'ternaryMid', handler: h.ternaryStart },
36
+ semicolon: { handler: h.semicolon }
37
+ }
38
+
25
39
  /**
26
40
  * A mapping of all states in the finite state machine to a set of instructions
27
41
  * for handling or transitioning into other states. Each state can be handled
@@ -68,13 +82,7 @@ export const states: States = {
68
82
  }
69
83
  },
70
84
  expectBinOp: {
71
- tokenTypes: {
72
- binaryOp: { toState: 'expectOperand' },
73
- dot: { toState: 'traverse' },
74
- openBracket: { toState: 'filter' },
75
- question: { toState: 'ternaryMid', handler: h.ternaryStart },
76
- semicolon: { handler: h.semicolon }
77
- },
85
+ tokenTypes: afterOperand,
78
86
  completable: true
79
87
  },
80
88
  expectObjKey: {
@@ -90,23 +98,15 @@ export const states: States = {
90
98
  }
91
99
  },
92
100
  postArgs: {
93
- tokenTypes: {
94
- binaryOp: { toState: 'expectOperand' },
95
- dot: { toState: 'traverse' },
96
- openBracket: { toState: 'filter' },
97
- question: { toState: 'ternaryMid', handler: h.ternaryStart },
98
- semicolon: { handler: h.semicolon }
99
- },
101
+ tokenTypes: afterOperand,
100
102
  completable: true
101
103
  },
102
104
  identifier: {
103
105
  tokenTypes: {
104
- binaryOp: { toState: 'expectOperand' },
105
- dot: { toState: 'traverse' },
106
- openBracket: { toState: 'filter' },
107
- openParen: { toState: 'argVal', handler: h.functionCall },
108
- question: { toState: 'ternaryMid', handler: h.ternaryStart },
109
- semicolon: { handler: h.semicolon }
106
+ ...afterOperand,
107
+ // a name is the only operand that can be followed by an argument list,
108
+ // since a function is called by name rather than evaluated as a value
109
+ openParen: { toState: 'argVal', handler: h.functionCall }
110
110
  },
111
111
  completable: true
112
112
  },