All files / for / parse.ts

100.00% Branches 125/125
100.00% Functions 13/13
100.00% Lines 184/184
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x14
 
x14
x115
x115
 
 
x14
x115
x115
x115
 
 
x14
 
 
x14
 
 
x14
x115
x114
x114
x61
 
x61
x11
x11
x5
x5
x5
x11
x61
x61
x50
x50
x61
x114
x94
x115
 
 
x14
x186
x186
x118
x118
x183
x186
 
 
x14
x20
x20
x47
 
x24
x47
x1
x1
x47
x2
x2
x47
x7
x7
x7
x37
x37
x18
x18
x20
 
 
x14
x36
x36
x76
x76
x2
x2
x76
x2
x2
 
x76
x8
x4
x4
x4
x4
x8
x8
x2
x8
x5
x5
x76
x76
x64
x64
x16
x16
x6
x6
x6
x10
x10
x57
x57
x76
x34
x34
x36
 
 
x14
x169
x20
x20
x169
x36
x36
x113
x169
 
 
x14
x144
x100
x100
x44
x44
 
x144
x2
x2
x2
x2
x144
x144
x1
x1
x1
x144
x7
x7
x4
x4
x4
x4
x7
x144
 
x10
x44
x44
x144
 
 
x14
x120
x4
x4
x4
x4
x116
x120
 
 
x14
x73
x38
x38
x35
x35
x73
x72
x6
x6
x6
x6
x72
x35
x35
x73
 
 
x14
x1418
x1418
 
 
x14
x783
x783
x337
x249
x249
x783
x446
x446
x428
x428
x428
x446
x783
x106
x85
x85
x21
x21
x677
x677
x783
x14












































































































































































































































// 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("]")) {
      while (this.#consume(",", { optional: true })) {
        // Skip holes and trailing commas
      }
      if (this.#peek("]")) {
        break
      }
      if (this.#spreading()) {
        break
      }
      if (this.#nested()) {
        this.#value()
        continue
      }
      this.#identifiers.push(this.#identifier())
    }
    this.#consume("]")
    return true
  }

  /** Consume object. */
  #object() {
    this.#consume("{")
    while (!this.#peek("}")) {
      this.#consume(",", { optional: true })
      if (this.#peek("}")) {
        break
      }
      if (this.#spreading()) {
        break
      }
      // Handle computed, string and numeric properties
      if (this.#peek(/^[[\d"']/)) {
        if (this.#peek("[")) {
          this.#group()
        } else {
          this.#consume(/^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\d[\p{L}\p{N}$_.]*)/u)
        }
        this.#consume(":")
        if (this.#nested()) {
          this.#value()
        } else {
          this.#identifiers.push(this.#identifier())
        }
      } // Handle regular properties
      else {
        let identifier = this.#identifier()
        if (this.#peek(":")) {
          this.#consume(":")
          if (this.#nested()) {
            this.#value()
            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
  }
}