From 221116b45298e51e80287ccffa2ae864ae98267d Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Sun, 29 Dec 2024 18:13:36 +0100 Subject: [PATCH] Fix await vs. class property initializers bug (https://github.com/acornjs/acorn/issues/1334) + add tests --- src/Acornima/Parser.State.cs | 22 ++--- src/Acornima/Parser.Statement.cs | 19 ++-- test/Acornima.Tests/ParserTests.cs | 143 +++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 20 deletions(-) diff --git a/src/Acornima/Parser.State.cs b/src/Acornima/Parser.State.cs index df2f1c4..3e793ba 100644 --- a/src/Acornima/Parser.State.cs +++ b/src/Acornima/Parser.State.cs @@ -261,22 +261,16 @@ private bool CanAwait get { - for (var i = _scopeStack.Count - 1; i >= 0; i--) + ref var scope = ref CurrentVarScope; + if ((scope._flags & ScopeFlags.Function) != 0) { - ref readonly var scope = ref _scopeStack.GetItemRef(i); - - if ((scope._flags & (ScopeFlags.InClassFieldInit | ScopeFlags.ClassStaticBlock)) != 0) - { - return false; - } - - if ((scope._flags & ScopeFlags.Function) != 0) - { - return (scope._flags & ScopeFlags.Async) != 0; - } + return (scope._flags & (ScopeFlags.Async | ScopeFlags.InClassFieldInit)) == ScopeFlags.Async; } - - return _options._allowAwaitOutsideFunction || _topLevelAwaitAllowed; + if ((scope._flags & ScopeFlags.Top) != 0) + { + return (_options._allowAwaitOutsideFunction || _topLevelAwaitAllowed) && (scope._flags & ScopeFlags.InClassFieldInit) == 0; + } + return false; } } diff --git a/src/Acornima/Parser.Statement.cs b/src/Acornima/Parser.Statement.cs index ebdba02..74f20ab 100644 --- a/src/Acornima/Parser.Statement.cs +++ b/src/Acornima/Parser.Statement.cs @@ -1439,15 +1439,22 @@ private ClassProperty ParseClassField(in Marker startMarker, Expression key, boo if (Eat(TokenType.Eq)) { // To raise SyntaxError if 'arguments' exists in the initializer. - var thisScopeIndex = CurrentScope._currentThisScopeIndex; - ref var scope = ref _scopeStack.GetItemRef(thisScopeIndex); - var oldScopeFlags = scope._flags; - scope._flags |= ScopeFlags.InClassFieldInit; + ref var currentScope = ref CurrentScope; + var thisScopeIndex = currentScope._currentThisScopeIndex; + var varScopeIndex = currentScope._currentVarScopeIndex; + + ref var thisScopeFlags = ref _scopeStack.GetItemRef(thisScopeIndex)._flags; + ref var varScopeFlags = ref _scopeStack.GetItemRef(varScopeIndex)._flags; + + var oldThisScopeFlags = thisScopeFlags; + var oldVarScopeFlags = varScopeFlags; + thisScopeFlags |= ScopeFlags.InClassFieldInit; + varScopeFlags |= ScopeFlags.InClassFieldInit; value = ParseMaybeAssign(ref NullRef()); - scope = ref _scopeStack.GetItemRef(thisScopeIndex); - scope._flags = oldScopeFlags; + _scopeStack.GetItemRef(thisScopeIndex)._flags = oldThisScopeFlags; + _scopeStack.GetItemRef(varScopeIndex)._flags = oldVarScopeFlags; } else { diff --git a/test/Acornima.Tests/ParserTests.cs b/test/Acornima.Tests/ParserTests.cs index ba0db0b..28294a0 100644 --- a/test/Acornima.Tests/ParserTests.cs +++ b/test/Acornima.Tests/ParserTests.cs @@ -814,6 +814,149 @@ public void ShouldHandleStrictModeDetectionEdgeCases(string input, bool isModule } } + [Theory] + [InlineData("script", "(class { x = () => arguments })", EcmaVersion.Latest, "'arguments' is not allowed in class field initializer or static initialization block")] + [InlineData("script", "() => { (class { x = () => arguments }) }", EcmaVersion.Latest, "'arguments' is not allowed in class field initializer or static initialization block")] + [InlineData("script", "() => class { x = () => { arguments } }", EcmaVersion.Latest, "'arguments' is not allowed in class field initializer or static initialization block")] + [InlineData("script", "() => class { x = function() { arguments } }", EcmaVersion.Latest, null)] + public void ShouldHandleArgumentsEdgeCases(string sourceType, string input, EcmaVersion ecmaVersion, string? expectedError) + { + var parser = new Parser(new ParserOptions { EcmaVersion = ecmaVersion }); + var parseAction = GetParseActionFor(sourceType); + + if (expectedError is null) + { + Assert.NotNull(parseAction(parser, input)); + } + else + { + var ex = Assert.Throws(() => parseAction(parser, input)); + Assert.Equal(expectedError, ex.Description); + } + } + + [Theory] + [InlineData("script", "(class { x = () => new.target })", EcmaVersion.Latest, null)] + [InlineData("script", "() => { (class { x = () => new.target }) }", EcmaVersion.Latest, null)] + [InlineData("script", "() => class { x = () => { new.target } }", EcmaVersion.Latest, null)] + [InlineData("script", "() => class { x = function() { new.target } }", EcmaVersion.Latest, null)] + public void ShouldHandleNewTargetEdgeCases(string sourceType, string input, EcmaVersion ecmaVersion, string? expectedError) + { + var parser = new Parser(new ParserOptions { EcmaVersion = ecmaVersion }); + var parseAction = GetParseActionFor(sourceType); + + if (expectedError is null) + { + Assert.NotNull(parseAction(parser, input)); + } + else + { + var ex = Assert.Throws(() => parseAction(parser, input)); + Assert.Equal(expectedError, ex.Description); + } + } + + [Theory] + [InlineData("script", "(class { x = () => super.y })", EcmaVersion.Latest, null)] + [InlineData("script", "() => { (class { x = () => super.y }) }", EcmaVersion.Latest, null)] + [InlineData("script", "() => class { x = () => { super.y } }", EcmaVersion.Latest, null)] + [InlineData("script", "() => class { x = function() { super.y } }", EcmaVersion.Latest, "'super' keyword unexpected here")] + [InlineData("script", "class C { x = class extends super.constructor { [super.constructor.name] = super.constructor } }", EcmaVersion.Latest, null)] + [InlineData("script", "() => class { x = class extends super.constructor { [super.constructor.name] = super.constructor } }", EcmaVersion.Latest, null)] + public void ShouldHandleSuperKeywordEdgeCases(string sourceType, string input, EcmaVersion ecmaVersion, string? expectedError) + { + var parser = new Parser(new ParserOptions { EcmaVersion = ecmaVersion }); + var parseAction = GetParseActionFor(sourceType); + + if (expectedError is null) + { + Assert.NotNull(parseAction(parser, input)); + } + else + { + var ex = Assert.Throws(() => parseAction(parser, input)); + Assert.Equal(expectedError, ex.Description); + } + } + + [Theory] + [InlineData("script", "(class { x = await })", EcmaVersion.Latest, null)] + [InlineData("module", "(class { x = await })", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "(class { x = await 1 })", EcmaVersion.Latest, "await is only valid in async functions and the top level bodies of modules")] + [InlineData("module", "(class { x = await 1 })", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "(class { x = () => await })", EcmaVersion.Latest, null)] + [InlineData("module", "(class { x = () => await })", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "(class { x = () => await 1 })", EcmaVersion.Latest, "await is only valid in async functions and the top level bodies of modules")] + [InlineData("module", "(class { x = () => await 1 })", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "(class { x = async () => await })", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("module", "(class { x = async () => await })", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("script", "(class { x = async () => await 1 })", EcmaVersion.Latest, null)] + [InlineData("module", "(class { x = async () => await 1 })", EcmaVersion.Latest, null)] + + [InlineData("script", "() => class { x = await }", EcmaVersion.Latest, null)] + [InlineData("module", "() => class { x = await }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "() => class { x = await 1 }", EcmaVersion.Latest, "await is only valid in async functions and the top level bodies of modules")] + [InlineData("module", "() => class { x = await 1 }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "() => class { x = () => await }", EcmaVersion.Latest, null)] + [InlineData("module", "() => class { x = () => await }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "() => class { x = () => await 1 }", EcmaVersion.Latest, "await is only valid in async functions and the top level bodies of modules")] + [InlineData("module", "() => class { x = () => await 1 }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "() => class { x = async () => await }", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("module", "() => class { x = async () => await }", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("script", "() => class { x = async () => await 1 }", EcmaVersion.Latest, null)] + [InlineData("module", "() => class { x = async () => await 1 }", EcmaVersion.Latest, null)] + + [InlineData("script", "async () => class { x = await }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = await }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "async () => class { x = await 1 }", EcmaVersion.Latest, "Unexpected number")] + [InlineData("module", "async () => class { x = await 1 }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "async () => class { x = () => await }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = () => await }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "async () => class { x = () => await 1 }", EcmaVersion.Latest, "Unexpected number")] + [InlineData("module", "async () => class { x = () => await 1 }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "async () => class { x = async () => await }", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("module", "async () => class { x = async () => await }", EcmaVersion.Latest, "Unexpected token '}'")] + [InlineData("script", "async () => class { x = async () => await 1 }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = async () => await 1 }", EcmaVersion.Latest, null)] + + [InlineData("script", "async () => class { x = (a = await) => a }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = (a = await) => a }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "async () => class { x = (a = await 1) => a }", EcmaVersion.Latest, "Unexpected number")] + [InlineData("module", "async () => class { x = (a = await 1) => a }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "async () => class { x = class await { y = await } }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = class await { y = await } }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "async () => class { x = class await { y = await 1 } }", EcmaVersion.Latest, "await is only valid in async functions and the top level bodies of modules")] + [InlineData("module", "async () => class { x = class await { y = await 1 } }", EcmaVersion.Latest, "Unexpected reserved word")] + + [InlineData("script", "async () => class { x = () => { { try {} catch (await) { } } } }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = () => { { try {} catch (await) { } } } }", EcmaVersion.Latest, "Unexpected reserved word")] + [InlineData("script", "async () => class { x = () => { { try {} catch { var await = 1 } } } }", EcmaVersion.Latest, null)] + [InlineData("module", "async () => class { x = () => { { try {} catch { var await = 1 } } } }", EcmaVersion.Latest, "Unexpected reserved word")] + public void ShouldHandleAwaitInClassFieldInitializer(string sourceType, string input, EcmaVersion ecmaVersion, string? expectedError) + { + // See also: https://github.com/acornjs/acorn/issues/1334, https://github.com/acornjs/acorn/issues/1338 + + var parser = new Parser(new ParserOptions { EcmaVersion = ecmaVersion }); + var parseAction = GetParseActionFor(sourceType); + + if (expectedError is null) + { + Assert.NotNull(parseAction(parser, input)); + } + else + { + var ex = Assert.Throws(() => parseAction(parser, input)); + Assert.Equal(expectedError, ex.Description); + } + } + [Theory] [InlineData("script", "await", EcmaVersion.Latest, null)] [InlineData("script", "await", EcmaVersion.ES13, null)]