Skip to content

Commit b4ae0d2

Browse files
baseballyamamarijnh
authored andcommitted
Add support for using and await using
FEATURE: Support `using` and `await using` syntax. Issue #1369
1 parent fdfb45a commit b4ae0d2

File tree

6 files changed

+2869
-16
lines changed

6 files changed

+2869
-16
lines changed

acorn-loose/src/statement.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,19 @@ lp.parseStatement = function() {
5454
this.expect(tt.parenL)
5555
if (this.tok.type === tt.semi) return this.parseFor(node, null)
5656
let isLet = this.toks.isLet()
57-
if (isLet || this.tok.type === tt._var || this.tok.type === tt._const) {
58-
let init = this.parseVar(this.startNode(), true, isLet ? "let" : this.tok.value)
57+
let isAwaitUsing = this.toks.isAwaitUsing(true)
58+
let isUsing = !isAwaitUsing && this.toks.isUsing(true)
59+
60+
if (isLet || this.tok.type === tt._var || this.tok.type === tt._const || isUsing || isAwaitUsing) {
61+
let kind = isLet ? "let" : isUsing ? "using" : isAwaitUsing ? "await using" : this.tok.value
62+
let init = this.startNode()
63+
if (isUsing || isAwaitUsing) {
64+
if (isAwaitUsing) this.next()
65+
this.parseVar(init, true, kind)
66+
} else {
67+
init = this.parseVar(init, true, kind)
68+
}
69+
5970
if (init.declarations.length === 1 && (this.tok.type === tt._in || this.isContextual("of"))) {
6071
if (this.options.ecmaVersion >= 9 && this.tok.type !== tt._in) {
6172
node.await = isAwait
@@ -196,6 +207,16 @@ lp.parseStatement = function() {
196207
this.next()
197208
return this.parseFunction(node, true, true)
198209
}
210+
211+
if (this.toks.isUsing(false)) {
212+
return this.parseVar(node, false, "using")
213+
}
214+
215+
if (this.toks.isAwaitUsing(false)) {
216+
this.next()
217+
return this.parseVar(node, false, "await using")
218+
}
219+
199220
let expr = this.parseExpression()
200221
if (isDummy(expr)) {
201222
this.next()

acorn/src/acorn.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export interface FunctionDeclaration extends Function {
169169
export interface VariableDeclaration extends Node {
170170
type: "VariableDeclaration"
171171
declarations: Array<VariableDeclarator>
172-
kind: "var" | "let" | "const"
172+
kind: "var" | "let" | "const" | "using" | "await using"
173173
}
174174

175175
export interface VariableDeclarator extends Node {
@@ -600,7 +600,7 @@ export function tokenizer(input: string, options: Options): {
600600
[Symbol.iterator](): Iterator<Token>
601601
}
602602

603-
export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | "latest"
603+
export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 | "latest"
604604

605605
export interface Options {
606606
/**

acorn/src/statement.js

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,49 @@ pp.isAsyncFunction = function() {
7272
!(isIdentifierChar(after = this.input.charCodeAt(next + 8)) || after > 0xd7ff && after < 0xdc00))
7373
}
7474

75+
pp.isUsingKeyword = function(isAwaitUsing, isFor) {
76+
if (this.options.ecmaVersion < 17 || !this.isContextual(isAwaitUsing ? "await" : "using"))
77+
return false
78+
79+
skipWhiteSpace.lastIndex = this.pos
80+
let skip = skipWhiteSpace.exec(this.input)
81+
let next = this.pos + skip[0].length
82+
83+
if (lineBreak.test(this.input.slice(this.pos, next))) return false
84+
85+
if (isAwaitUsing) {
86+
let awaitEndPos = next + 5 /* await */, after
87+
if (this.input.slice(next, awaitEndPos) !== "using" ||
88+
awaitEndPos === this.input.length ||
89+
isIdentifierChar(after = this.input.charCodeAt(awaitEndPos)) ||
90+
(after > 0xd7ff && after < 0xdc00)
91+
) return false
92+
93+
skipWhiteSpace.lastIndex = awaitEndPos
94+
let skipAfterUsing = skipWhiteSpace.exec(this.input)
95+
if (skipAfterUsing && lineBreak.test(this.input.slice(awaitEndPos, awaitEndPos + skipAfterUsing[0].length))) return false
96+
}
97+
98+
if (isFor) {
99+
let ofEndPos = next + 2 /* of */, after
100+
if (this.input.slice(next, ofEndPos) === "of") {
101+
if (ofEndPos === this.input.length ||
102+
(!isIdentifierChar(after = this.input.charCodeAt(ofEndPos)) && !(after > 0xd7ff && after < 0xdc00))) return false
103+
}
104+
}
105+
106+
let ch = this.input.charCodeAt(next)
107+
return isIdentifierStart(ch, true) || ch === 92 // '\'
108+
}
109+
110+
pp.isAwaitUsing = function(isFor) {
111+
return this.isUsingKeyword(true, isFor)
112+
}
113+
114+
pp.isUsing = function(isFor) {
115+
return this.isUsingKeyword(false, isFor)
116+
}
117+
75118
// Parse a single statement.
76119
//
77120
// If expecting a statement and finding a slash operator, parse a
@@ -148,6 +191,23 @@ pp.parseStatement = function(context, topLevel, exports) {
148191
return this.parseFunctionStatement(node, true, !context)
149192
}
150193

194+
let usingKind = this.isAwaitUsing(false) ? "await using" : this.isUsing(false) ? "using" : null
195+
if (usingKind) {
196+
if (topLevel && this.options.sourceType === "script") {
197+
this.raise(this.start, "Using declaration cannot appear in the top level when source type is `script`")
198+
}
199+
if (usingKind === "await using") {
200+
if (!this.canAwait) {
201+
this.raise(this.start, "Await using cannot appear outside of async function")
202+
}
203+
this.next()
204+
}
205+
this.next()
206+
this.parseVar(node, false, usingKind)
207+
this.semicolon()
208+
return this.finishNode(node, "VariableDeclaration")
209+
}
210+
151211
let maybeName = this.value, expr = this.parseExpression()
152212
if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon))
153213
return this.parseLabeledStatement(node, maybeName, expr, context)
@@ -223,18 +283,19 @@ pp.parseForStatement = function(node) {
223283
this.next()
224284
this.parseVar(init, true, kind)
225285
this.finishNode(init, "VariableDeclaration")
226-
if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
227-
if (this.options.ecmaVersion >= 9) {
228-
if (this.type === tt._in) {
229-
if (awaitAt > -1) this.unexpected(awaitAt)
230-
} else node.await = awaitAt > -1
231-
}
232-
return this.parseForIn(node, init)
233-
}
234-
if (awaitAt > -1) this.unexpected(awaitAt)
235-
return this.parseFor(node, init)
286+
return this.parseForAfterInit(node, init, awaitAt)
236287
}
237288
let startsWithLet = this.isContextual("let"), isForOf = false
289+
290+
let usingKind = this.isUsing(true) ? "using" : this.isAwaitUsing(true) ? "await using" : null
291+
if (usingKind) {
292+
let init = this.startNode()
293+
this.next()
294+
if (usingKind === "await using") this.next()
295+
this.parseVar(init, true, usingKind)
296+
this.finishNode(init, "VariableDeclaration")
297+
return this.parseForAfterInit(node, init, awaitAt)
298+
}
238299
let containsEsc = this.containsEsc
239300
let refDestructuringErrors = new DestructuringErrors
240301
let initPos = this.start
@@ -260,6 +321,20 @@ pp.parseForStatement = function(node) {
260321
return this.parseFor(node, init)
261322
}
262323

324+
// Helper method to parse for loop after variable initialization
325+
pp.parseForAfterInit = function(node, init, awaitAt) {
326+
if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
327+
if (this.options.ecmaVersion >= 9) {
328+
if (this.type === tt._in) {
329+
if (awaitAt > -1) this.unexpected(awaitAt)
330+
} else node.await = awaitAt > -1
331+
}
332+
return this.parseForIn(node, init)
333+
}
334+
if (awaitAt > -1) this.unexpected(awaitAt)
335+
return this.parseFor(node, init)
336+
}
337+
263338
pp.parseFunctionStatement = function(node, isAsync, declarationPosition) {
264339
this.next()
265340
return this.parseFunction(node, FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT), false, isAsync)
@@ -511,6 +586,8 @@ pp.parseVar = function(node, isFor, kind, allowMissingInitializer) {
511586
decl.init = this.parseMaybeAssign(isFor)
512587
} else if (!allowMissingInitializer && kind === "const" && !(this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) {
513588
this.unexpected()
589+
} else if (!allowMissingInitializer && (kind === "using" || kind === "await using") && this.options.ecmaVersion >= 17 && this.type !== tt._in && !this.isContextual("of")) {
590+
this.raise(this.lastTokEnd, `Missing initializer in ${kind} declaration`)
514591
} else if (!allowMissingInitializer && decl.id.type !== "Identifier" && !(isFor && (this.type === tt._in || this.isContextual("of")))) {
515592
this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value")
516593
} else {
@@ -523,7 +600,10 @@ pp.parseVar = function(node, isFor, kind, allowMissingInitializer) {
523600
}
524601

525602
pp.parseVarId = function(decl, kind) {
526-
decl.id = this.parseBindingAtom()
603+
decl.id = kind === "using" || kind === "await using"
604+
? this.parseIdent()
605+
: this.parseBindingAtom()
606+
527607
this.checkLValPattern(decl.id, kind === "var" ? BIND_VAR : BIND_LEXICAL, false)
528608
}
529609

bin/test262.unsupported-features

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
decorators
2-
explicit-resource-management
32
import-defer
43
source-phase-imports

test/run.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
require("./tests-class-features-2022.js");
3131
require("./tests-module-string-names.js");
3232
require("./tests-import-attributes.js");
33+
require("./tests-using.js");
3334
var acorn = require("../acorn")
3435
var acorn_loose = require("../acorn-loose")
3536

0 commit comments

Comments
 (0)