All files / for / parse.ts

100.00% Branches 59/59
100.00% Lines 169/169
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x11
 
x11
x92
x92
 
 
x11
x92
x92
x92
 
 
x11
 
 
x11
 
 
x11
x92
x172
x172
x636
 
x212
x223
x223
x228
x684
x228
x223
x212
x212
x241
x241
x212
x172
x155
x92
 
 
x11
x141
x141
x224
x224
x268
x141
 
 
x11
x27
x27
x195
x65
x67
x67
x65
x70
x70
x96
x96
x41
x41
x27
 
 
x11
x34
x34
x258
x86
x88
x88
 
x86
x89
x89
x89
x90
x90
x86
x86
x133
x133
x137
x137
x139
x139
x139
x139
x177
x177
x86
x55
x55
x34
 
 
x11
x124
x140
x140
x124
x147
x147
x198
x124
 
 
x11
x105
x160
x160
x144
x144
 
x105
x107
x107
x107
x107
x105
x105
x143
x143
x143
x105
x112
x112
x116
x116
x116
x116
x112
x105
 
x111
x144
x144
x105
 
 
x11
x101
x105
x315
x105
x105
x187
x101
 
 
x11
x74
x107
x107
x162
x810
x74
x203
x209
x209
x209
x209
x203
x162
x162
x74
 
 
x11
x944
x944
 
 
x11
x556
x556
x780
x956
x956
x556
x877
x877
x1183
x1183
x1183
x877
x556
x619
x664
x664
x637
x637
x1038
x1038
x556
x11




























































































































































































































// Imports
import type { Optional } from "@libs/typing"

/**
 * Expression parser for loops content.
 *
 * Parse any valid expression that can be evaluated in EcmaScript by `for (<expression>)` syntax.
 *
 * This simple AST does not guarantee syntax validity (which will be enforced by runtime anyways),
 * but it should be able to extract any defined identifiers, whether they're initialized through destructuring, spreading, aliasing, etc.
 *
 * {@link https://tc39.es/ecma262/#sec-for-statement | Reference}
 *
 * ```ts
 * import { Expression } from "./parse.ts"
 *
 * const identifiers = Expression.parse("const {a, b:c} of []")
 * console.assert(identifiers.join(",") === ["a", "c"].join(","))
 * ```
 *
 * @author Simon Lecoq (lowlighter)
 * @license MIT
 */
export class Expression {
  /** Parse expression. */
  static parse(expression: string): string[] {
    return new Expression(expression).#parse()
  }

  /** Constructor. */
  private constructor(expression: string) {
    this.#expression = expression.trim()
    this.#identifiers = [] as string[]
  }

  /** Expression. */
  #expression

  /** Collected identifiers. */
  #identifiers

  /** Parse expressions. */
  #parse() {
    if (!this.#peek(";")) {
      this.#consume(/^(?:let|const|var) /)
      if (!this.#nested()) {
        this.#identifiers.push(this.#identifier({ assignment: false }))
        // Handle regular `for` loops
        if (this.#peek("=")) {
          this.#value(/^[\s\S]*?(?=[,;])/)
          while (this.#peek(",")) {
            this.#consume(",")
            this.#identifiers.push(this.#identifier({ value: /^[\s\S]*?(?=[,;])/ }))
          }
          this.#consume(/;[\s\S]*;[\s\S]*$/)
        } // Handle `for..of` and `for..in`
        else {
          this.#consume(/^(?:of|in) /)
        }
      }
    }
    return this.#identifiers
  }

  /** Consume identifier. */
  #identifier({ assignment = true, value = undefined as Optional<RegExp> } = {}) {
    const identifier = this.#consume(/^([\p{L}$_][\p{L}\p{N}$_]*)/u)
    if (assignment) {
      this.#value(value)
    }
    return identifier
  }

  /** Consume array. */
  #array() {
    this.#consume("[")
    while (!this.#peek("]")) {
      this.#consume(",", { optional: true })
      if (this.#spreading()) {
        break
      }
      if (this.#nested()) {
        continue
      }
      this.#identifiers.push(this.#identifier())
    }
    this.#consume("]")
    return true
  }

  /** Consume object. */
  #object() {
    this.#consume("{")
    while (!this.#peek("}")) {
      this.#consume(",", { optional: true })
      if (this.#spreading()) {
        break
      }
      // Handle computed properties
      if (this.#peek("[")) {
        this.#group()
        this.#consume(":")
        if (!this.#nested()) {
          this.#identifiers.push(this.#identifier())
        }
      } // Handle regular properties
      else {
        let identifier = this.#identifier()
        if (this.#peek(":")) {
          this.#consume(":")
          if (this.#nested()) {
            continue
          }
          identifier = this.#identifier()
        }
        this.#identifiers.push(identifier)
      }
    }
    this.#consume("}")
    return true
  }

  /** Consume nested element. */
  #nested() {
    if (this.#peek("[")) {
      return this.#array()
    }
    if (this.#peek("{")) {
      return this.#object()
    }
    return false
  }

  /** Consume value. */
  #value(value = /^[\s\S]*?(?=[,\]}])/) {
    if (!this.#peek("=")) {
      return false
    }
    this.#consume("=")
    let f = false
    // Handle regular functions
    if (this.#peek(/^(?:async\s+)?function(?:\s*\*)?\s*\(/)) {
      this.#consume(/^(?:async\s+)?function(?:\s*\*)?/)
      f = true
      this.#group(/^(\()/)
      this.#group(/^(\{)/)
    } // Handle rrow functions
    else if (this.#peek(/^async\s+\(/)) {
      this.#consume("async")
      f = true
    }
    if (this.#peek("(")) {
      this.#group(/^(\()/)
      if (this.#peek("=>")) {
        this.#consume("=>")
        this.#group()
        f = true
      }
    }
    while ((!f) && (this.#group())) {
      // Continue capturing groups to match case like {foo:"bar"}[i], (x => y)(), etc.
    }
    this.#consume(value)
    return true
  }

  /** Consume spreading. */
  #spreading() {
    if (this.#peek("...")) {
      this.#consume("...")
      this.#identifiers.push(this.#identifier({ assignment: false }))
      return true
    }
    return false
  }

  /** Consume group. */
  #group(opener = /^([({[])/) {
    if (!this.#peek(opener)) {
      return false
    }
    const open = this.#consume(opener)
    const close = { "(": ")", "{": "}", "[": "]" }[open] as string
    for (let i = 0; (i < this.#expression.length) && (close !== this.#expression[i]); i++) {
      if (opener.test(this.#expression[i])) {
        this.#expression = this.#expression.slice(i)
        this.#group()
        break
      }
    }
    this.#expression = this.#expression.slice(this.#expression.indexOf(close) + 1).trim()
    return true
  }

  /** Peek at next token. */
  #peek(token: RegExp | string) {
    return typeof token === "string" ? this.#expression.startsWith(token) : token.test(this.#expression)
  }

  /** Consume token. */
  #consume(token: RegExp | string, { optional = false } = {}) {
    let captured = null
    if (typeof token === "string") {
      if (this.#expression.startsWith(token)) {
        captured = token
      }
    } else {
      const match = this.#expression.match(token)
      if (match) {
        captured = match[1]
        token = match[0]
      }
    }
    if (captured === null) {
      if (optional) {
        return ""
      }
      throw new SyntaxError(`Expected ${token} at ${this.#expression}`)
    }
    this.#expression = this.#expression.replace(token, "").trim()
    return captured
  }
}