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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x10
 
x10
x90
x90
 
 
x10
x90
x90
x90
 
 
x10
 
 
x10
 
 
x10
x90
x169
x169
x627
 
x209
x220
x220
x225
x675
x225
x220
x209
x209
x238
x238
x209
x169
x153
x90
 
 
x10
x140
x140
x223
x223
x267
x140
 
 
x10
x26
x26
x192
x64
x66
x66
x64
x69
x69
x95
x95
x40
x40
x26
 
 
x10
x33
x33
x255
x85
x87
x87
 
x85
x88
x88
x88
x89
x89
x85
x85
x132
x132
x136
x136
x138
x138
x138
x138
x176
x176
x85
x54
x54
x33
 
 
x10
x123
x139
x139
x123
x146
x146
x197
x123
 
 
x10
x104
x159
x159
x143
x143
 
x104
x106
x106
x106
x106
x104
x104
x142
x142
x142
x104
x111
x111
x115
x115
x115
x115
x111
x104
 
x110
x143
x143
x104
 
 
x10
x100
x104
x312
x104
x104
x186
x100
 
 
x10
x73
x106
x106
x161
x805
x73
x202
x208
x208
x208
x208
x202
x161
x161
x73
 
 
x10
x942
x942
 
 
x10
x554
x554
x778
x954
x954
x554
x874
x874
x1180
x1180
x1180
x874
x554
x616
x661
x661
x633
x633
x1036
x1036
x554
x10




























































































































































































































// 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) {
    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
  }
}