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

DanielXMoore / Civet / 21042909916

15 Jan 2026 07:01PM UTC coverage: 91.616% (+0.01%) from 91.606%
21042909916

Pull #1838

github

web-flow
Merge 0420daaf1 into 5f9b32981
Pull Request #1838: Infer partial object/array types from initializers

3775 of 4114 branches covered (91.76%)

Branch coverage included in aggregate %.

62 of 62 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

19174 of 20935 relevant lines covered (91.59%)

17395.67 hits per line

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

96.01
/source/parser/util.civet
1
import type {
1✔
2
  ASTLeaf
1✔
3
  ASTNode
1✔
4
  ASTNodeObject
1✔
5
  ASTNodeParent
1✔
6
  ASTString
1✔
7
  Children
1✔
8
  FunctionNode
1✔
9
  FunctionSignature
1✔
10
  Identifier
1✔
11
  IsParent
1✔
12
  IsToken
1✔
13
  IterationStatement
1✔
14
  Literal
1✔
15
  Parameter
1✔
16
  ParametersNode
1✔
17
  Parent
1✔
18
  RegularExpressionLiteral
1✔
19
  ReturnTypeAnnotation
1✔
20
  StatementNode
1✔
21
  StatementTuple
1✔
22
  TemplateLiteral
1✔
23
  TypeNode
1✔
24
  TypeSuffix
1✔
25
} from ./types.civet
1✔
26

1✔
27
import {
1✔
28
  gatherRecursiveWithinFunction
1✔
29
  type Predicate
1✔
30
} from ./traversal.civet
1✔
31

1✔
32
// Types need to be upfront to allow for TypeScript `asserts`
1✔
33
assert: {
1✔
34
  equal(a: unknown, b: unknown, msg: string): asserts a is b
1✔
35
  notEqual(a: unknown, b: unknown, msg: string): void
1✔
36
  notNull<T>(a: T, msg: string): asserts a is NonNullable<T>
1✔
37
} := {
1✔
38
  equal(a, b, msg): void
21,928✔
39
    /* c8 ignore next */
1✔
40
    throw new Error(`Assertion failed [${msg}]: ${a} !== ${b}`) if a !== b
1✔
41
  notEqual(a, b, msg): void
1,757✔
42
    /* c8 ignore next */
1✔
43
    throw new Error(`Assertion failed [${msg}]: ${a} === ${b}`) if a === b
1✔
44
  notNull(a, msg)
103✔
45
    /* c8 ignore next */
1✔
46
    throw new Error(`Assertion failed [${msg}]: got null`) unless a?
1✔
47
}
1✔
48

1✔
49
/**
1✔
50
 * Adds parent pointers to all nodes in the AST. Elements within
1✔
51
 * arrays of nodes receive the closest non-array object parent.
1✔
52
 */
1✔
53
function addParentPointers(node: ASTNode, parent?: ASTNodeObject): void
252,037✔
54
  if (not node?) return
252,037✔
55
  if (typeof node !== "object") return
252,037✔
56

191,195✔
57
  // NOTE: Arrays are transparent and skipped when traversing via parent
191,195✔
58
  if Array.isArray(node)
191,195✔
59
    for child of node
70,521✔
60
      addParentPointers(child, parent)
108,583✔
61
    return
70,521✔
62

120,674✔
63
  node = node as ASTNodeObject
120,674✔
64
  node.parent = parent if parent?
252,037✔
65
  if node.children
120,674✔
66
    for each child of node.children
49,977✔
67
      addParentPointers(child, node)
140,207✔
68

1✔
69
/**
1✔
70
 * Clone an AST node including children (removing parent pointes)
1✔
71
 * This gives refs new identities which may not be what we want.
1✔
72
 *
1✔
73
 * TODO: preserve ref identities
1✔
74
 */
1✔
75
function clone(node: ASTNode)
12✔
76
  removeParentPointers(node)
12✔
77
  return deepCopy(node)
12✔
78

1✔
79
function removeParentPointers(node: ASTNode): void
101✔
80
  return unless node? <? "object"
99✔
81

89✔
82
  // NOTE: Arrays are transparent and skipped when traversing via parent
89✔
83
  if Array.isArray node
89✔
84
    for child of node
6✔
85
      removeParentPointers child
10✔
86
    return
6✔
87

83✔
88
  node.parent = null
83✔
89
  if node.children
83✔
90
    for child of node.children
43✔
91
      removeParentPointers child
79✔
92

1✔
93
// If node is not an ASTNodeObject, wrap it in a special "Wrapper" node
1✔
94
// so that it can have a parent (second argument).
1✔
95
function maybeWrap(node: ASTNode, parent: ASTNodeObject?): ASTNodeObject
209✔
96
  unless isASTNodeObject node
209✔
97
    updateParentPointers node = {
28✔
98
      type: 'Wrapper'
28✔
99
      children: [ node ]
28✔
100
      parent
28✔
101
    }
28✔
102
  node
209✔
103

1✔
104
// Unwrap "Wrapper" node if there is one.
1✔
105
// You should call `updateParentPointers` after this gets assigned somewhere.
1✔
106
function maybeUnwrap(node: ASTNode): ASTNode
218✔
107
  if node?.type is "Wrapper"
218✔
108
    node.children[0]
28✔
109
  else
190✔
110
    node
190✔
111

1✔
112
function isASTNodeObject(node: ASTNode): node is ASTNodeObject
2,361✔
113
  (and)
2,361✔
114
    node <? "object"
2,361✔
115
    node?
2,355✔
116
    not Array.isArray node
2,355✔
117

1✔
118
function isParent(node: ASTNode): node is ASTNode & IsParent
22,101✔
119
  node? and node.children?
22,101✔
120

1✔
121
function isToken(node: ASTNode): node is ASTNode & IsToken
10,709✔
122
  node? and node.token?
10,709✔
123

1✔
124
function isEmptyBareBlock(node: ASTNode): boolean
16✔
125
  if (node?.type !== "BlockStatement") return false
16!
126
  { bare, expressions } := node
16✔
127
  return bare and
16✔
128
    expressions is like [], [ [, {type: "EmptyStatement"}, ...] ]
16✔
129

1✔
130
function isFunction(node: ASTNode): node is FunctionNode
268,646✔
131
  if { type } := node
268,450✔
132
    return (or)
118,606✔
133
      type is "FunctionExpression"
118,606✔
134
      type is "ArrowFunction"
117,547✔
135
      type is "MethodDefinition"
116,387✔
136

150,040✔
137
  return false
150,040✔
138

1✔
139
// Keep this in sync with StatementNode in types.civet
1✔
140
statementTypes := new Set [
1✔
141
  "BlockStatement"
1✔
142
  "BreakStatement"
1✔
143
  "ComptimeStatement"
1✔
144
  "ContinueStatement"
1✔
145
  "DebuggerStatement"
1✔
146
  "Declaration"
1✔
147
  "DoStatement"
1✔
148
  "ForStatement"
1✔
149
  "IfStatement"
1✔
150
  "IterationStatement"
1✔
151
  "LabelledStatement"
1✔
152
  "ReturnStatement"
1✔
153
  "SwitchStatement"
1✔
154
  "ThrowStatement"
1✔
155
  "TryStatement"
1✔
156
]
1✔
157
function isStatement(node: ASTNode): node is StatementNode
256✔
158
  (and)
256✔
159
    isASTNodeObject node
256✔
160
    node.type?  // forbid leaf
192✔
161
    statementTypes.has node.type
192✔
162

1✔
163
function isWhitespaceOrEmpty(node): boolean
2,727✔
164
  if (!node) return true
2,727✔
165
  if (node.type is "Ref") return false
2,727✔
166
  if (node.token) return /^\s*$/.test(node.token)
2,727✔
167
  if (node.children) node = node.children
2,727✔
168
  if (!node.length) return true
2,727✔
169
  if (typeof node is "string") return /^\s*$/.test(node)
2,727✔
170
  if (Array.isArray(node)) return node.every(isWhitespaceOrEmpty)
1,123✔
171
  return false
×
172

1✔
173
// Returns leading space as a string, or undefined if none
1✔
174
function firstNonSpace(node: ASTNode): ASTNode
569✔
175
  return unless node?
569✔
176
  if Array.isArray node
566✔
177
    for each child of node
227✔
178
      if first := firstNonSpace child
232✔
179
        return first
224✔
180
    return undefined
3✔
181
  else if isParent node
339✔
182
    if first := firstNonSpace node.children
220✔
183
      return first
220✔
184
    else
×
185
      return node
×
186
  else if isToken node
119✔
187
    return if node.token is like /^[ \t]*$/
109✔
188
  else if node <? "string"
10✔
189
    return if node is like /^[ \t]*$/
9✔
190
  node
117✔
191

1✔
192
/**
1✔
193
 * Does this statement force exit from normal flow, or loop forever,
1✔
194
 * implying that the line after this one can never execute?
1✔
195
 */
1✔
196
function isExit(node: ASTNode): boolean
3,271✔
197
  return false unless node?
3,271✔
198
  switch node.type
3,244✔
199
    // Exit from normal flow
3,244✔
200
    when "ReturnStatement", "ThrowStatement", "BreakStatement", "ContinueStatement"
306✔
201
      true
306✔
202
    // if checks then and else clause
3,271✔
203
    when "IfStatement"
3,271✔
204
      (or)
249✔
205
        // `insertReturn` for IfStatement adds a return to children
249✔
206
        // when there's no else block
249✔
207
        node.children.-1?.type is "ReturnStatement"
249✔
208
        (node.children.-1 as StatementTuple)?[1]?.type is "ReturnStatement"
249✔
209
        (and)
248✔
210
          isExit node.then
248✔
211
          isExit node.else?.block
54✔
212
    when "PatternMatchingStatement"
3,271✔
213
      isExit node.children[0][1]
17✔
214
    when "SwitchStatement"
3,271✔
215
      (and)
35✔
216
        // Every clause should exit, or continue to next clause
35✔
217
        for every clause of node.caseBlock.clauses
35✔
218
          if clause.type is like "CaseClause", "WhenClause", "DefaultClause"
6✔
219
            // `break` might jump to end of switch, so don't consider an exit
37✔
220
            not (clause.type is "WhenClause" and clause.break) and
37✔
221
            not gatherRecursiveWithinFunction(clause.block, .type is "BreakStatement")#
8✔
222
          else
3✔
223
            isExit clause.block
3✔
224
        // Ensure exhaustive by requiring an else/default clause
1✔
225
        for some clause, i of node.caseBlock.clauses
1✔
226
          clause.type is "DefaultClause" and
1✔
227
          // Require default clause to exit or continue to next clause
1✔
228
          // (checked above) and eventually reach an exiting clause
1✔
229
          for some later of node.caseBlock.clauses[i..]
1✔
230
            later.type is like "CaseClause", "WhenClause", "DefaultClause" and
2✔
231
            isExit later.block
2✔
232
    when "TryStatement"
3,271✔
233
      // Require all non-finally blocks to exit
17✔
234
      node.blocks.every isExit
17✔
235
    when "BlockStatement"
3,271✔
236
      // [1] extracts statement from [indent, statement, delim]
432✔
237
      node.expressions.some (s) => isExit s[1]
432✔
238
    // Infinite loops
3,271✔
239
    when "IterationStatement"
3,271✔
240
      (and)
39✔
241
        isLoopStatement node
39✔
242
        gatherRecursiveWithinFunction(node.block,
569✔
243
          .type is "BreakStatement").length is 0
23✔
244
        // TODO: Distinguish between break of this loop vs. break of inner loops
3,271✔
245
    else
3,271✔
246
      false
2,149✔
247

1✔
248
// Is this a `loop` statement, or equivalent `while(true)`?
1✔
249
function isLoopStatement(node: ASTNodeObject): node is IterationStatement
120✔
250
  (and)
120✔
251
    node.type is "IterationStatement"
120✔
252
    node.condition?.type is "ParenthesizedExpression"
41✔
253
    node.condition.expression?.type is "Literal"
41✔
254
    node.condition.expression?.raw is "true"
25✔
255

1✔
256
/**
1✔
257
 * Detects Comma, CommaDelimiter, and ParameterElementDelimiter
1✔
258
 * with an explicit comma, as should be at the top level of
1✔
259
 * a "Call" node's `args` array.
1✔
260
 * Returns the node whose `token` is ",", or else undefined.
1✔
261
 */
1✔
262
function isComma(node: ASTNode): (ASTLeaf & { token: "," }) | undefined
377✔
263
  if node?.token is ","
377✔
264
    node
222✔
265
  else if Array.isArray(node) and node.-1?.token is ","
155✔
266
    node.-1
26✔
267

1✔
268
function stripTrailingImplicitComma(children: ASTNode[])
234✔
269
  last := children.-1
234✔
270
  if isComma(last) and last.implicit
234✔
271
    children[...-1]
197✔
272
  else
37✔
273
    children
37✔
274

1✔
275
function hasTrailingComment(node: ASTNode): boolean
576✔
276
  return false unless node?
576✔
277
  return true if node.type is "Comment"
576✔
278
  if Array.isArray node
379✔
279
    return hasTrailingComment node.-1
376✔
280
  if "children" in node
3✔
281
    return hasTrailingComment (node.children as Children).-1
3✔
282
  false
×
283

1✔
284
/**
1✔
285
 * Trims the first single space from the spacing array or node's children if present
1✔
286
 * Inserts string `c` in the first position.
1✔
287
 * maintains $loc for source maps
1✔
288
 */
1✔
289
function insertTrimmingSpace<T extends ASTNode>(target: T, c: string): T
15,505✔
290
  return target unless target?
15,505✔
291

15,476✔
292
  if Array.isArray target
15,476✔
293
    return c if target.length is 0
7,976✔
294

7,730✔
295
    for each e, i of target
7,730✔
296
      if i is 0
11,849✔
297
        insertTrimmingSpace(e, c)
7,730✔
298
      else
4,119✔
299
        e
4,119✔
300
  else if isParent target
7,500✔
301
    oldChildren := target.children
2,160✔
302
    target = {
2,160✔
303
      ...target
2,160✔
304
      children: insertTrimmingSpace target.children, c
2,160✔
305
    }
2,160✔
306
    // Preserve aliased properties
2,160✔
307
    for key in target
2,160✔
308
      i := oldChildren.indexOf target[key]
8,340✔
309
      if i >= 0
8,340✔
310
        target[key] = target.children[i]
868✔
311
    target
2,160✔
312
  else if isToken target
5,340✔
313
    {
5,292✔
314
      ...target
5,292✔
315
      token: target.token.replace(/^ ?/, c)
5,292✔
316
    }
5,292✔
317
  else if target <? "string"
48✔
318
    target.replace(/^ ?/, c)
24✔
319
  else
24✔
320
    target
24✔
321

1✔
322
/**
1✔
323
 * Trims the first single space from the spacing array or node's children if present
1✔
324
 * maintains $loc for source maps
1✔
325
 */
1✔
326
function trimFirstSpace<T extends ASTNode>(target: T): T
5,612✔
327
  insertTrimmingSpace target, ""
5,612✔
328

1✔
329
function inplaceInsertTrimmingSpace(target: ASTNode, c: string): void
143✔
330
  return target unless target?
143✔
331
  if Array.isArray target
140✔
332
    inplaceInsertTrimmingSpace target[0], c
72✔
333
  else if isParent target
68✔
334
    inplaceInsertTrimmingSpace target.children, c
42✔
335
  else if isToken target
26✔
336
    target.token = target.token.replace(/^ ?/, c)
26✔
337

1✔
338
// Returns leading space as a string, or undefined if none
1✔
339
function getTrimmingSpace(target: ASTNode): string?
1,433✔
340
  return unless target?
1,433✔
341
  if Array.isArray target
1,396✔
342
    getTrimmingSpace target[0]
176✔
343
  else if isParent target
1,220✔
344
    getTrimmingSpace target.children[0]
757✔
345
  else if isToken target
463✔
346
    target.token.match(/^ ?/)![0]
326✔
347

1✔
348
// Glom content e.g. whitespace into AST nodes
1✔
349
// NOTE: This can get weird if we depend on the specific location of children
1✔
350
function prepend(prefix: ASTNode, node: ASTNode): ASTNode
43,254✔
351
  return node if not prefix or prefix is like []
43,254✔
352
  if Array.isArray node
5,344✔
353
    [prefix, ...node]
146✔
354
  else if isParent node
5,198✔
355
    {
5,198✔
356
      ...node
5,198✔
357
      children: [prefix, ...node.children]
5,198✔
358
    } as typeof node
5,198✔
359
  // NOTE: Not glomming into an AST leaf; would need to flatten prefix
×
360
  else
×
361
    [prefix, node]
×
362

1✔
363
function append(node: ASTNode, suffix: ASTNode): ASTNode
935✔
364
  return node if not suffix or suffix is like []
935✔
365
  if Array.isArray node
752✔
366
    [...node, suffix]
×
367
  else if isParent node
752✔
368
    {
507✔
369
      ...node
507✔
370
      children: [...node.children, suffix]
507✔
371
    } as typeof node
507✔
372
  // NOTE: Not glomming into an AST leaf; would need to flatten suffix
245✔
373
  else
245✔
374
    [node, suffix]
245✔
375

1✔
376
function inplacePrepend(prefix: ASTNode, node: ASTNode): void
16✔
377
  return unless prefix
16!
378
  return if Array.isArray(prefix) and not prefix.length
16!
379
  if Array.isArray node
16✔
380
    node.unshift prefix
4✔
381
  else if isParent node
12✔
382
    node.children.unshift prefix
12✔
383
  else
×
384
    throw new Error "Can't prepend to a leaf node"
×
385

1✔
386
// Convert (non-Template) Literal to actual JavaScript value
1✔
387
function literalValue(literal: Literal)
325✔
388
  { raw } .= literal
325✔
389
  switch raw
325✔
390
    case "null": return null
325!
391
    case "true": return true
325!
392
    case "false": return false
325!
393
  switch literal.subtype
325✔
394
    when "StringLiteral"
325✔
395
      assert.equal
94✔
396
        (or)
94✔
397
          raw.startsWith('"') and raw.endsWith('"')
94✔
398
          raw.startsWith("'") and raw.endsWith("'")
78✔
399
        true, "String literal should begin and end in single or double quotes"
94✔
400
      return raw[1...-1]
94✔
401
    when "NumericLiteral"
325✔
402
      raw = raw.replace(/_/g, "")
231✔
403
      if raw.endsWith("n")
231✔
404
        return BigInt raw[0...-1]
×
405
      else if raw.match(/[\.eE]/)
231✔
406
        return parseFloat(raw)
2✔
407
      else if [ , base ] := raw.match(/^[+-]?0(.)/)
229✔
408
        switch base.toLowerCase()
7✔
409
          case "x": return parseInt(raw.replace(/0[xX]/, ""), 16)
7✔
410
          case "b": return parseInt(raw.replace(/0[bB]/, ""), 2)
7✔
411
          case "o": return parseInt(raw.replace(/0[oO]/, ""), 8)
7✔
412
      return parseInt(raw, 10)
222✔
413
    else
325!
414
      throw new Error("Unrecognized literal " + JSON.stringify(literal))
×
415

1✔
416
/** TypeScript type for given literal */
1✔
417
function literalType(literal: Literal | RegularExpressionLiteral | TemplateLiteral): TypeNode
32✔
418
  let t: ASTString
32✔
419
  switch literal.type
32✔
420
    when "RegularExpressionLiteral"
32✔
421
      t = "RegExp"
2✔
422
    when "TemplateLiteral"
32✔
423
      t = "string"
2✔
424
    when "Literal"
32✔
425
      switch literal.subtype
28✔
426
        when "NullLiteral"
28!
UNCOV
427
          t = "null"
×
428
        when "BooleanLiteral"
28✔
429
          t = "boolean"
3✔
430
        when "NumericLiteral"
28✔
431
          if literal.raw.endsWith 'n'
23✔
432
            t = "bigint"
1✔
433
          else
22✔
434
            t = "number"
22✔
435
        when "StringLiteral"
28✔
436
          t = "string"
2✔
437
        else
28!
438
          throw new Error `unknown literal subtype ${literal.subtype}`
×
439
    else
32!
440
      throw new Error `unknown literal type ${literal.type}`
×
441
  {}
32✔
442
    type: "TypeLiteral"
32✔
443
    t
32✔
444
    children: [t]
32✔
445

1✔
446
/**
1✔
447
 * Attempt to infer TypeScript type suffix from expression,
1✔
448
 * similar to TypeScript's inferred type for `x` in `let x = expression`.
1✔
449
 * Returns undefined if the expression is too complicated,
1✔
450
 * or something TypeScript wouldn't infer like `null` or `undefined`.
1✔
451
 */
1✔
452
function typeOfExpression(expression: ASTNode): TypeNode?
50✔
453
  let t: TypeNode
50✔
454
  return unless isASTNodeObject expression
50!
455
  switch expression.type
50✔
456
    when "Literal"
50✔
457
      switch expression.subtype
30✔
458
        when "NullLiteral"
30✔
459
          // `let x = null` doesn't infer type for `x`
2✔
460
          return
2✔
461
        else
30✔
462
          t = literalType expression
28✔
463
    when "RegularExpressionLiteral", "TemplateLiteral"
4✔
464
      t = literalType expression
4✔
465
    when "Identifier"
50✔
466
      // `let x = undefined` doesn't infer type for `x`
12✔
467
      return if expression.name is "undefined"
12✔
468
      continue switch
12✔
469
    when "MemberExpression"
50✔
470
      t = {}
11✔
471
        type: "TypeTypeof"
11✔
472
        children: ["typeof ", expression]
11✔
473
        expression
11✔
474
    else
50✔
475
      // TypeScript's typeof doesn't support arguments
3✔
476
      // beyond identifiers and member expressions
3✔
477
      return
3✔
478
  t
43✔
479

1✔
480
function typeSuffixForExpression(expression: ASTNode): TypeSuffix?
39✔
481
  t := typeOfExpression expression
39✔
482
  return unless t?
39✔
483
  {
35✔
484
    type: "TypeSuffix"
35✔
485
    ts: true
35✔
486
    t
35✔
487
    children: [": ", t]
35✔
488
  }
35✔
489

1✔
490
function makeNumericLiteral(n: number): Literal
32✔
491
  s := n.toString()
32✔
492
  type: "Literal"
32✔
493
  subtype: "NumericLiteral"
32✔
494
  raw: s
32✔
495
  children: [
32✔
496
    {
32✔
497
      type: "NumericLiteral"
32✔
498
      token: s
32✔
499
    } as NumericLiteral // missing $loc
32✔
500
  ]
32✔
501

1✔
502
/**
1✔
503
* Detect whether the first nontrivial string/token matches a given regular
1✔
504
* expression. You probably want the RegExp to start with `^`.
1✔
505
*/
1✔
506
function startsWith(target: ASTNode, value: RegExp)
3,786✔
507
  if (!target) return
3,786!
508
  if Array.isArray target
3,786✔
509
    let i = 0
2,060✔
510
    let l = target.length
2,060✔
511
    while i < l
2,060✔
512
      const t = target[i]
2,428✔
513
      break if t and (t.length or t.token or t.children)
2,428✔
514
      i++
389✔
515
    if i < l
2,060✔
516
      return startsWith target[i], value
2,039✔
517
  if (typeof target is "string") return value.test target
3,786✔
518
  if (target.children) return startsWith target.children, value
3,786✔
519
  if (target.token) return value.test target.token
21✔
520

1✔
521
function startsWithPredicate<T extends ASTNodeObject>(node: ASTNode, predicate: Predicate<T>, skip = isWhitespaceOrEmpty): T | undefined
1,495✔
522
  return undefined if not node? or node <? "string"
1,495✔
523

1,479✔
524
  if Array.isArray node
1,479✔
525
    for each child of node
679✔
526
      continue if skip child
720✔
527
      return startsWithPredicate child, predicate
679✔
528
    return
×
529

800✔
530
  return node if predicate node
1,495✔
531
  return unless node.children?
1,495✔
532
  startsWithPredicate node.children, predicate
539✔
533

1✔
534
/**
1✔
535
 * Does this expression have an `await` in it and thus needs to be `async`?
1✔
536
 * Skips over nested functions, which have their own async behavior.
1✔
537
 */
1✔
538
function hasAwait(exp: ASTNode)
4,438✔
539
  gatherRecursiveWithinFunction(exp, .type is "Await").length > 0
4,438✔
540

1✔
541
function hasYield(exp: ASTNode)
4,429✔
542
  gatherRecursiveWithinFunction(exp, .type is "Yield").length > 0
4,429✔
543

1✔
544
function hasImportDeclaration(exp: ASTNode)
×
545
  gatherRecursiveWithinFunction(exp, .type is "ImportDeclaration").length > 0
×
546

1✔
547
function hasExportDeclaration(exp: ASTNode)
×
548
  gatherRecursiveWithinFunction(exp, .type is "ExportDeclaration").length > 0
×
549

1✔
550
/**
1✔
551
* Copy an AST node deeply, including children.
1✔
552
* Ref nodes maintain identity
1✔
553
* Preserves aliasing
1✔
554
*/
1✔
555
function deepCopy<T extends ASTNode>(root: T): T
78✔
556
  copied := new Map<ASTNode, ASTNode>
78✔
557
  return recurse(root) as T
78✔
558

78✔
559
  function recurse(node: ASTNode): ASTNode
78✔
560
    return node unless node? <? "object"
1,570✔
561

754✔
562
    unless copied.has node
754✔
563
      if Array.isArray node
721✔
564
        array: ASTNode[] := new Array node#
243✔
565
        copied.set node, array
243✔
566
        for each item, i of node
243✔
567
          array[i] = recurse item
397✔
568
      else if node?.type is "Ref"
478✔
569
        copied.set node, node
6✔
570
      else
472✔
571
        obj: any := {}
472✔
572
        copied.set node, obj
472✔
573
        for key in node
472✔
574
          value := (node as any)[key]
1,266✔
575
          if key is "parent"
1,266✔
576
            // If parent is within deep copy, use that
127✔
577
            // Otherwise, don't copy to avoid copying outside subtree
127✔
578
            obj.parent = copied.get(value) ?? value
127✔
579
          else
1,139✔
580
            obj[key] = recurse value
1,139✔
581

754✔
582
    copied.get node
754✔
583

1✔
584
/**
1✔
585
 * Replace this node with another, by modifying its parent's children.
1✔
586
 */
1✔
587
function replaceNode(node: ASTNode, newNode: ASTNode, parent?: ASTNodeParent): void
460✔
588
  parent ??= (node as ASTNodeObject?)?.parent
460✔
589
  unless parent?
460✔
590
    throw new Error "replaceNode failed: node has no parent"
×
591

460✔
592
  function recurse(children: ASTNode[]): boolean
460✔
593
    for each child, i of children
1,865✔
594
      if child is node
3,460✔
595
        children[i] = newNode
460✔
596
        return true
460✔
597
      else if Array.isArray child
3,000✔
598
        return true if recurse child
1,405✔
599
    return false
1,029✔
600

460✔
601
  unless recurse parent.children
460✔
602
    throw new Error "replaceNode failed: didn't find child node in parent"
×
603

460✔
604
  // Adjust 'expression' etc. alias pointers
460✔
605
  for key, value in parent
460✔
606
    if value is node
2,198✔
607
      parent[key] = newNode
220✔
608

460✔
609
  if isASTNodeObject newNode
460✔
610
    newNode.parent = parent
447✔
611
  // Don't destroy node's parent, as we often include it within newNode
1✔
612
  //node.parent = undefined
1✔
613

1✔
614
/**
1✔
615
 * Replace all nodes that match predicate with replacer(node)
1✔
616
 */
1✔
617
function replaceNodes(root, predicate, replacer)
163✔
618
  return root unless root?
163!
619

163✔
620
  array := Array.isArray(root) ? root : root.children
163✔
621

163✔
622
  unless array
163✔
623
    if predicate root
64✔
624
      return replacer root, root
×
625
    else
64✔
626
      return root
64✔
627

99✔
628
  for each node, i of array
99✔
629
    return unless node?
157!
630
    if predicate node
157✔
631
      array[i] = replacer node, root
31✔
632
    else
126✔
633
      replaceNodes node, predicate, replacer
126✔
634

99✔
635
  return root
99✔
636

1✔
637
/**
1✔
638
* When cloning subtrees sometimes we need to remove hoistDecs
1✔
639
*/
1✔
640
function removeHoistDecs(node: ASTNode): void
101✔
641
  if (not node?) return
101✔
642
  if (typeof node !== "object") return
101✔
643

89✔
644
  if "hoistDec" in node
89✔
645
    node.hoistDec = undefined
1✔
646

89✔
647
  // NOTE: Arrays are transparent and skipped when traversing via parent
89✔
648
  if Array.isArray(node)
89✔
649
    for child of node
6✔
650
      removeHoistDecs(child)
10✔
651
    return
6✔
652

83✔
653
  if node.children
83✔
654
    for child of node.children
43✔
655
      removeHoistDecs(child)
79✔
656

1✔
657
skipParens := new Set [
1✔
658
  "AmpersandRef"
1✔
659
  "ArrayExpression"
1✔
660
  "CallExpression"
1✔
661
  "Identifier"
1✔
662
  "JSXElement"
1✔
663
  "JSXFragment"
1✔
664
  "Literal"
1✔
665
  "ParenthesizedExpression"
1✔
666
  "Ref"
1✔
667
  "Placeholder"
1✔
668
  "StatementExpression" // wrapIIFE
1✔
669
]
1✔
670

1✔
671
/**
1✔
672
 * Convert general ExtendedExpression into LeftHandSideExpression.
1✔
673
 * More generally wrap in parentheses if necessary.
1✔
674
 * (Consider renaming and making parentheses depend on context.)
1✔
675
 */
1✔
676
function makeLeftHandSideExpression(expression: ASTNode)
1,386✔
677
  while [ item ] := expression
1,386✔
678
    expression = item
3✔
679
  if isASTNodeObject expression
1,386✔
680
    return expression if expression.token
1,334✔
681
    return expression if expression.parenthesized
1,334!
682
    return expression if skipParens.has expression.type
1,334✔
683
    // `new Foo` and `new Foo.Bar` need parenthesization;
807✔
684
    // `new Foo(args)` does not
807✔
685
    return expression if expression.type is "NewExpression" and
1,334✔
686
      expression.expression.children.some ?.type is "Call"
×
687
    return expression if expression.type is "MemberExpression" and
1,334✔
688
      not startsWithPredicate(expression, .type is "ObjectExpression")
21✔
689
  parenthesizeExpression expression
838✔
690

1✔
691
function parenthesizeExpression(expression: ASTNode)
853✔
692
  makeNode {
853✔
693
    type: "ParenthesizedExpression"
853✔
694
    children: ["(", expression, ")"]
853✔
695
    expression
853✔
696
    implicit: true
853✔
697
  }
853✔
698

1✔
699
// If the expression is not a valid left-hand side for assignment,
1✔
700
// add an Error node to it and return true.
1✔
701
// TOMAYBE: Static semantics https://262.ecma-international.org/#sec-static-semantics-assignmenttargettype
1✔
702
function checkValidLHS(node: ASTNode): boolean
1,061✔
703
  switch node?.type
1,061✔
704
    when "UnaryExpression"
1,061✔
705
      node.children.unshift
1✔
706
        type: "Error"
1✔
707
        message: "Unary expression is not a valid left-hand side"
1✔
708
      return true
1✔
709
    when "CallExpression"
1,061✔
710
      lastType := node.children.-1?.type
6✔
711
      switch lastType
6✔
712
        when "PropertyAccess", "SliceExpression", "Index"
3✔
713
        else
6✔
714
          node.children.splice -1, 0,
3✔
715
            type: "Error"
3✔
716
            message: `Call expression ending with ${lastType} is not a valid left-hand side`
3✔
717
          return true
3✔
718
    when "Placeholder"
1,061✔
719
      node.children.unshift
2✔
720
        type: "Error"
2✔
721
        message: `Lone placeholder (${node.subtype}) is not a valid left-hand side`
2✔
722
      return true
2✔
723
  return false
1,055✔
724

1✔
725
/**
1✔
726
 * Just update parent pointers for the children of a node,
1✔
727
 * recursing into arrays but not objects.  More efficient version of
1✔
728
 * `addParentPointers` when just injecting one new node.
1✔
729
 */
1✔
730
function updateParentPointers(node: ASTNode, parent?: ASTNodeParent, depth = 1): void
51,962✔
731
  return unless node?
51,962✔
732
  return unless node <? "object"
51,962✔
733

36,800✔
734
  // NOTE: Arrays are transparent and skipped when traversing via parent
36,800✔
735
  if Array.isArray(node)
36,800✔
736
    for each child of node
12,149✔
737
      updateParentPointers(child, parent, depth)
23,985✔
738
    return
12,149✔
739

24,651✔
740
  node = node as ASTNodeObject
24,651✔
741
  node.parent = parent if parent?
51,962✔
742
  if depth and isParent node
51,962✔
743
    for each child of node.children
6,962✔
744
      updateParentPointers(child, node, depth-1)
21,042✔
745

1✔
746
function makeNode<T extends ASTNodeObject>(node: T): T
5,323✔
747
  updateParentPointers node
5,323✔
748
  node
5,323✔
749

1✔
750
/**
1✔
751
 * Used to ignore the result of __ if it is only whitespace
1✔
752
 * Useful to preserve spacing around comments
1✔
753
 */
1✔
754
function skipIfOnlyWS(target)
2,007✔
755
  if (!target) return target
2,007✔
756
  if (Array.isArray(target)) {
2,007✔
757
    if (target.length is 1) {
468✔
758
      return skipIfOnlyWS(target[0])
338✔
759
    } else if (target.every((e) => (skipIfOnlyWS(e) is undefined))) {
468✔
760
      return undefined
115✔
761
    }
115✔
762
    return target
15✔
763
  }
15✔
764
  if (target.token != null and target.token.trim() is '') {
2,007✔
765
    return undefined
520✔
766
  }
520✔
767
  return target
228✔
768

1✔
769
/**
1✔
770
 * Splice child from children/array, similar to Array.prototype.splice,
1✔
771
 * but specifying a child instead of an index.  Throw if child not found.
1✔
772
 */
1✔
773
function spliceChild(node: ASTNodeObject | ASTNode[], child: ASTNode, del: number, ...replacements: ASTNode[])
42✔
774
  children := Array.isArray(node) ? node : node.children
42!
775
  unless Array.isArray children
42✔
776
    throw new Error "spliceChild: non-array node has no children field"
×
777
  index := children.indexOf child
42✔
778
  if index < 0
42✔
779
    throw new Error "spliceChild: child not found"
×
780
  children.splice index, del, ...replacements
42✔
781

1✔
782
/**
1✔
783
 * Convert type suffix of `?: T` to `: undefined | T`
1✔
784
 */
1✔
785
function convertOptionalType(suffix: TypeSuffix | ReturnTypeAnnotation): void
14✔
786
  if suffix.t?.type is "TypeAsserts"
14✔
787
    spliceChild suffix, suffix.optional, 1, suffix.optional =
1✔
788
      type: "Error"
1✔
789
      message: "Can't use optional ?: syntax with asserts type"
1✔
790
    return
1✔
791
  spliceChild suffix, suffix.optional, 1, suffix.optional = undefined
13✔
792
  // Return types with | need to be wrapped in parentheses
13✔
793
  wrap := suffix.type is "ReturnTypeAnnotation"
13✔
794
  spliceChild suffix, suffix.t, 1, suffix.t = [
13✔
795
    getTrimmingSpace suffix.t
13✔
796
    wrap ? "(" : undefined
14✔
797
    // TODO: avoid parens if unnecessary
14✔
798
    "undefined | "
14✔
799
    parenthesizeType trimFirstSpace suffix.t
14✔
800
    wrap ? ")" : undefined
14✔
801
  ]
14✔
802

1✔
803
const typeNeedsNoParens = new Set [
1✔
804
  "TypeIdentifier"
1✔
805
  "ImportType"
1✔
806
  "TypeLiteral"
1✔
807
  "TypeTuple"
1✔
808
  "TypeParenthesized"
1✔
809
]
1✔
810
/**
1✔
811
 * Parenthesize type if it might need it in some contexts.
1✔
812
 */
1✔
813
function parenthesizeType(type: ASTNode)
29✔
814
  return type if typeNeedsNoParens.has type.type
29✔
815
  makeNode
6✔
816
    type: "TypeParenthesized"
6✔
817
    ts: true
6✔
818
    children: ["(", type, ")"]
6✔
819

1✔
820
/**
1✔
821
 * Wrap expressions in an IIFE, adding async/await if expressions
1✔
822
 * use await, or just adding async if specified.
1✔
823
 * Uses function* instead of arrow function if given generator star.
1✔
824
 * Returns an Array suitable for `children`.
1✔
825
 */
1✔
826
function wrapIIFE(expressions: StatementTuple[], asyncFlag?: boolean, generatorStar?: ASTNode): ASTNode
171✔
827
  let awaitPrefix: ASTNode?
171✔
828
  generator := generatorStar ? [ generatorStar ] : []
171✔
829

171✔
830
  async := []
171✔
831
  if asyncFlag
171✔
832
    async.push "async "
18✔
833
  else if hasAwait expressions
153✔
834
    async.push "async "
3✔
835
    awaitPrefix =
3✔
836
      type: "Await"
3✔
837
      children: ["await "]
3✔
838

171✔
839
  yieldWrap .= false
171✔
840
  unless generator#
171✔
841
    if hasYield expressions
156✔
842
      generator.push "*"
2✔
843
      yieldWrap = true
2✔
844

171✔
845
  block := makeNode {
171✔
846
    type: "BlockStatement"
171✔
847
    expressions
171✔
848
    children: ["{", expressions, "}"]
171✔
849
    bare: false
171✔
850
    root: false
171✔
851
  }
171✔
852

171✔
853
  parameterList: Parameter[] := []
171✔
854
  parameters: ParametersNode :=
171✔
855
    type: "Parameters"
171✔
856
    children: ["(", parameterList, ")"]
171✔
857
    parameters: parameterList
171✔
858
    names: []
171✔
859

171✔
860
  signature: FunctionSignature := {}
171✔
861
    type: "FunctionSignature"
171✔
862
    modifier:
171✔
863
      async: !!async#
171✔
864
      generator: !!generator#
171✔
865
    parameters
171✔
866
    returnType: undefined
171✔
867
    implicitReturn: true // force implicit return in IIFE
171✔
868
    children:
171✔
869
      generator# ? [ async, "function", generator, parameters ]
171✔
870
                 : [ async, parameters ]
171✔
871

171✔
872
  let fn
171✔
873
  if generator#
171✔
874
    fn = makeNode {
17✔
875
      type: "FunctionExpression"
17✔
876
      signature
17✔
877
      parameters
17✔
878
      returnType: undefined
17✔
879
      async
17✔
880
      block
17✔
881
      generator
17✔
882
      children: [ ...signature.children, block ]
17✔
883
    }
17✔
884
  else
154✔
885
    fn = makeNode {
154✔
886
      type: "ArrowFunction"
154✔
887
      signature
154✔
888
      parameters
154✔
889
      returnType: undefined
154✔
890
      async
154✔
891
      block
154✔
892
      children: [ ...signature.children, "=>", block ]
154✔
893
    }
154✔
894

171✔
895
  children := [ makeLeftHandSideExpression(fn), "()" ]
171✔
896

171✔
897
  if fn.type is "FunctionExpression"
171✔
898
    if gatherRecursiveWithinFunction(block, (is like { token: "this" }))#
17✔
899
      children.splice 1, 0, ".bind(this)"
2✔
900
    if gatherRecursiveWithinFunction(block, (is like { token: "arguments" }))#
17✔
901
      binding: Identifier :=
1✔
902
        type: "Identifier"
1✔
903
        name: "arguments"
1✔
904
        names: ["arguments"]
1✔
905
        children: ["arguments"]
1✔
906
      parameterList.push {}
1✔
907
        type: "Parameter"
1✔
908
        children: [binding]
1✔
909
        names: ["arguments"]
1✔
910
        binding
1✔
911
      children.-1 = "(arguments)"
1✔
912

171✔
913
  exp: ASTNode .= makeNode {
171✔
914
    type: "CallExpression"
171✔
915
    children
171✔
916
  }
171✔
917

171✔
918
  if yieldWrap
171✔
919
    // yield* supports async iterators, so no need to await here
2✔
920
    exp = makeLeftHandSideExpression makeNode
2✔
921
      type: "YieldExpression"
2✔
922
      star: "*"
2✔
923
      expression: exp
2✔
924
      children:
2✔
925
        . type: "Yield"
2✔
926
          children: ["yield"]
2✔
927
        . "*", " "
2✔
928
        . exp
2✔
929
  else if awaitPrefix
169✔
930
    exp = makeLeftHandSideExpression [awaitPrefix, exp]
2✔
931

171✔
932
  return exp
171✔
933

1✔
934
function wrapWithReturn(expression?: ASTNode, parent: Parent = expression?.parent, semi = false): ASTNode
628✔
935
  children := expression ? ["return ", expression] : ["return"]
628✔
936
  children.unshift ";" if semi
628✔
937

628✔
938
  return makeNode {
628✔
939
    type: "ReturnStatement"
628✔
940
    children
628✔
941
    expression
628✔
942
    parent
628✔
943
  }
628✔
944

1✔
945
function flatJoin<T, S>(array: T[][], separator: S): (T | S)[]
260✔
946
  result := []
260✔
947
  for each items, i of array
260✔
948
    result.push separator if i
505✔
949
    result.push ...items
505✔
950
  result
260✔
951

1✔
952
export {
1✔
953
  addParentPointers
1✔
954
  append
1✔
955
  assert
1✔
956
  checkValidLHS
1✔
957
  clone
1✔
958
  convertOptionalType
1✔
959
  deepCopy
1✔
960
  firstNonSpace
1✔
961
  flatJoin
1✔
962
  getTrimmingSpace
1✔
963
  hasAwait
1✔
964
  hasExportDeclaration
1✔
965
  hasImportDeclaration
1✔
966
  hasTrailingComment
1✔
967
  hasYield
1✔
968
  inplaceInsertTrimmingSpace
1✔
969
  inplacePrepend
1✔
970
  insertTrimmingSpace
1✔
971
  isASTNodeObject
1✔
972
  isComma
1✔
973
  isEmptyBareBlock
1✔
974
  isExit
1✔
975
  isFunction
1✔
976
  isLoopStatement
1✔
977
  isStatement
1✔
978
  isToken
1✔
979
  isWhitespaceOrEmpty
1✔
980
  literalValue
1✔
981
  literalType
1✔
982
  typeOfExpression
1✔
983
  typeSuffixForExpression
1✔
984
  makeLeftHandSideExpression
1✔
985
  makeNode
1✔
986
  makeNumericLiteral
1✔
987
  maybeWrap
1✔
988
  maybeUnwrap
1✔
989
  parenthesizeExpression
1✔
990
  parenthesizeType
1✔
991
  prepend
1✔
992
  removeHoistDecs
1✔
993
  removeParentPointers
1✔
994
  replaceNode
1✔
995
  replaceNodes
1✔
996
  skipIfOnlyWS
1✔
997
  spliceChild
1✔
998
  startsWith
1✔
999
  startsWithPredicate
1✔
1000
  stripTrailingImplicitComma
1✔
1001
  trimFirstSpace
1✔
1002
  updateParentPointers
1✔
1003
  wrapIIFE
1✔
1004
  wrapWithReturn
1✔
1005
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc