All files / for / parse.ts

100.00% Branches 109/109
100.00% Functions 13/13
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
x82
x82
 
 
x11
x82
x82
x82
 
 
x11
 
 
x11
 
 
x11
x82
x81
x81
x41
 
x41
x11
x11
x5
x5
x5
x11
x41
x41
x30
x30
x41
x81
x64
x82
 
 
x11
x131
x131
x83
x83
x128
x131
 
 
x11
x16
x16
x38
x38
x2
x2
x38
x5
x5
x31
x31
x14
x14
x16
 
 
x11
x23
x23
x52
x52
x2
x2
 
x52
x3
x3
x3
x1
x1
x52
x52
x47
x47
x4
x4
x2
x2
x2
x2
x44
x44
x52
x21
x21
x23
 
 
x11
x114
x16
x16
x114
x23
x23
x75
x114
 
 
x11
x94
x55
x55
x39
x39
 
x94
x2
x2
x2
x2
x94
x94
x1
x1
x1
x94
x7
x7
x4
x4
x4
x4
x7
x94
 
x6
x39
x39
x94
 
 
x11
x90
x4
x4
x4
x4
x86
x90
 
 
x11
x63
x33
x33
x30
x30
x63
x71
x6
x6
x6
x6
x71
x30
x30
x63
 
 
x11
x937
x937
 
 
x11
x548
x548
x224
x176
x176
x548
x324
x324
x309
x309
x309
x324
x548
x63
x45
x45
x18
x18
x485
x485
x548
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
  }
}