diff --git a/src/Acornima/Parser.State.cs b/src/Acornima/Parser.State.cs index aa4f0f6..a31d15e 100644 --- a/src/Acornima/Parser.State.cs +++ b/src/Acornima/Parser.State.cs @@ -105,7 +105,16 @@ internal void Reset(string input, int start, int length, SourceType sourceType, _scopeId = 0; _scopeStack.Clear(); - EnterScope(ScopeFlags.Top); + + // NOTE: Seeding the root scope with the super flags (as opposed to just short-circuiting the `AllowSuper`/`AllowDirectSuper` getters) + // makes both options follow the this binding: as `EnterScope` propagates the current this scope through arrow function scopes + // but not through ordinary function scopes, super is allowed exactly where the top level's this binding is in effect. + var superFlags = + _options._allowSuperCallOutsideConstructor ? ScopeFlags.Super | ScopeFlags.DirectSuper + : _options._allowSuperOutsideMethod ? ScopeFlags.Super + : ScopeFlags.None; + + EnterScope(ScopeFlags.Top | superFlags); _privateNameStack.Clear(); @@ -285,7 +294,7 @@ private bool AllowSuper // https://github.com/acornjs/acorn/blob/8.11.3/acorn/src/state.js > `get allowSuper` [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _options._allowSuperOutsideMethod || (CurrentThisScope._flags & (ScopeFlags.Super | ScopeFlags.InClassFieldInit)) != 0; + get => (CurrentThisScope._flags & (ScopeFlags.Super | ScopeFlags.InClassFieldInit)) != 0; } private bool AllowDirectSuper diff --git a/src/Acornima/ParserOptions.cs b/src/Acornima/ParserOptions.cs index d783d76..4d34b67 100644 --- a/src/Acornima/ParserOptions.cs +++ b/src/Acornima/ParserOptions.cs @@ -40,6 +40,7 @@ protected ParserOptions(ParserOptions original) _allowAwaitOutsideFunction = original._allowAwaitOutsideFunction; _allowNewTargetOutsideFunction = original._allowNewTargetOutsideFunction; _allowSuperOutsideMethod = original._allowSuperOutsideMethod; + _allowSuperCallOutsideConstructor = original._allowSuperCallOutsideConstructor; _checkPrivateFields = original._checkPrivateFields; _onInsertedSemicolon = original._onInsertedSemicolon; _onTrailingComma = original._onTrailingComma; @@ -119,15 +120,43 @@ public ExperimentalESFeatures ExperimentalESFeatures /// Gets or sets whether to allow new.target meta-properties in the top-level scope. /// Defaults to . /// + /// + /// This option is intended for parsing code which is evaluated in the context of a function + /// (see PerformEval and its inFunc parameter). + /// Enabling it means that new.target accesses are permitted at the top level of the parsed code. + /// public bool AllowNewTargetOutsideFunction { get => _allowNewTargetOutsideFunction; init => _allowNewTargetOutsideFunction = value; } internal readonly bool _allowSuperOutsideMethod; /// - /// Gets or sets whether to allow super identifiers to appear outside methods. + /// Gets or sets whether to allow super property accesses to appear outside methods. /// Defaults to . /// + /// + /// This option is intended for parsing code which is evaluated in the context of a method + /// (see PerformEval and its inMethod parameter). + /// Enabling it means that super property accesses are permitted at the top level of the parsed code, that is, + /// in the same places where this refers to the top level's this binding. + /// (E.g. they are allowed in arrow functions declared at the top level but not in ordinary functions.) + /// Direct super calls remain disallowed; see for those. + /// public bool AllowSuperOutsideMethod { get => _allowSuperOutsideMethod; init => _allowSuperOutsideMethod = value; } + internal readonly bool _allowSuperCallOutsideConstructor; + /// + /// Gets or sets whether to allow super calls to appear outside the constructor of a derived class. + /// Defaults to . + /// + /// + /// This option is intended for parsing code which is evaluated in the context of the constructor of a derived class + /// (see PerformEval and its inDerivedConstructor parameter). + /// Enabling it means that direct super calls (as well as super property accesses) are permitted at the top level + /// of the parsed code, that is, in the same places where this refers to the top level's this binding. + /// (E.g. they are allowed in arrow functions declared at the top level but not in ordinary functions.) + /// This option implies . + /// + public bool AllowSuperCallOutsideConstructor { get => _allowSuperCallOutsideConstructor; init => _allowSuperCallOutsideConstructor = value; } + internal readonly bool _allowTopLevelUsing; /// /// Gets or sets whether to allow using declarations to appear at the top level of scripts as well. diff --git a/test/Acornima.Tests/ParserTests.cs b/test/Acornima.Tests/ParserTests.cs index dd9ef69..83f1c27 100644 --- a/test/Acornima.Tests/ParserTests.cs +++ b/test/Acornima.Tests/ParserTests.cs @@ -983,6 +983,104 @@ public void ShouldHandleSuperKeywordEdgeCases(string sourceType, string input, E } } + [Theory] + // Direct super calls are not allowed at the top level unless AllowSuperCallOutsideConstructor is enabled. + // (AllowSuperOutsideMethod alone doesn't enable them.) + [InlineData("script", "super()", false, false, "'super' keyword unexpected here")] + [InlineData("script", "super()", true, false, "'super' keyword unexpected here")] + [InlineData("script", "super()", false, true, null)] + [InlineData("script", "super()", true, true, null)] + [InlineData("module", "super()", false, false, "'super' keyword unexpected here")] + [InlineData("module", "super()", false, true, null)] + [InlineData("expression", "super()", false, false, "'super' keyword unexpected here")] + [InlineData("expression", "super()", false, true, null)] + + // Arrow functions inherit the this binding of the top level, so direct super calls are allowed in them as well. + [InlineData("script", "(() => super())()", false, false, "'super' keyword unexpected here")] + [InlineData("script", "(() => super())()", false, true, null)] + [InlineData("script", "() => () => super()", false, true, null)] + [InlineData("script", "async () => super()", false, true, null)] + // (The argument of the nested eval call is just a string literal to the parser. When the host parses that string, + // the top level cases above apply to it.) + [InlineData("script", "(() => eval('super()'))()", false, false, null)] + [InlineData("script", "(() => eval('super()'))()", false, true, null)] + + // Ordinary functions introduce a this binding of their own, so direct super calls remain disallowed in them. + [InlineData("script", "function f() { super() }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "function f() { super() }", true, true, "'super' keyword unexpected here")] + [InlineData("script", "(function () { super() })", false, true, "'super' keyword unexpected here")] + [InlineData("script", "() => function () { super() }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "({ m() { super() } })", false, true, "'super' keyword unexpected here")] + + // Super property accesses are allowed wherever direct super calls are, and AllowSuperOutsideMethod allows + // exactly those, without allowing direct super calls. + [InlineData("script", "super.x", false, false, "'super' keyword unexpected here")] + [InlineData("script", "super.x", true, false, null)] + [InlineData("script", "super.x", false, true, null)] + [InlineData("script", "(() => super.x)()", false, true, null)] + [InlineData("script", "(() => super.x)()", true, false, null)] + // (Both options follow the top level's this binding, so neither of them reaches into an ordinary function.) + [InlineData("script", "function f() { super.x }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "function f() { super.x }", true, false, "'super' keyword unexpected here")] + [InlineData("script", "function f() { super.x }", true, true, "'super' keyword unexpected here")] + // (Methods bring a home object of their own, so they are unaffected by either option.) + [InlineData("script", "({ m() { super.x } })", false, false, null)] + [InlineData("script", "({ m() { super.x } })", true, false, null)] + + // Classes are unaffected: the constructor of a derived class remains the only place where direct super calls are allowed. + [InlineData("script", "class A extends B { constructor() { super() } }", false, false, null)] + [InlineData("script", "class A extends B { constructor() { super() } }", false, true, null)] + [InlineData("script", "class A { constructor() { super() } }", false, false, "'super' keyword unexpected here")] + [InlineData("script", "class A { constructor() { super() } }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "class A extends B { m() { super() } }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "class A extends B { constructor() { function f() { super() } } }", false, true, "'super' keyword unexpected here")] + [InlineData("script", "class C { x = super.y }", false, false, null)] + [InlineData("script", "class C { x = super.y }", false, true, null)] + // (Class field initializers don't introduce a this binding of their own, but they are never a place for a direct + // super call either: https://tc39.es/ecma262/#sec-class-definitions-static-semantics-early-errors makes it a + // Syntax Error if the Initializer Contains SuperCall. So neither the constructor of a derived class nor the + // option enables one there.) + [InlineData("script", "class A extends B { constructor() { class C { x = super() } } }", false, false, "'super' keyword unexpected here")] + [InlineData("script", "class C { x = super() }", false, false, "'super' keyword unexpected here")] + [InlineData("script", "class C { x = super() }", false, true, "'super' keyword unexpected here")] + // (A computed class element name, on the other hand, is evaluated in the enclosing scope, so it inherits + // whatever that scope allows.) + [InlineData("script", "class C { [super()]() { } }", false, false, "'super' keyword unexpected here")] + [InlineData("script", "class C { [super()]() { } }", false, true, null)] + public void ShouldHandleSuperCallOutsideConstructor(string sourceType, string input, bool allowSuperOutsideMethod, bool allowSuperCallOutsideConstructor, string? expectedError) + { + var parser = new Parser(new ParserOptions + { + AllowSuperOutsideMethod = allowSuperOutsideMethod, + AllowSuperCallOutsideConstructor = allowSuperCallOutsideConstructor, + }); + 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); + } + } + + [Fact] + public void AllowSuperCallOutsideConstructorShouldDefaultToFalse() + { + Assert.False(new ParserOptions().AllowSuperCallOutsideConstructor); + Assert.False(ParserOptions.Default.AllowSuperCallOutsideConstructor); + + var options = ParserOptions.Default with { AllowSuperCallOutsideConstructor = true }; + Assert.True(options.AllowSuperCallOutsideConstructor); + Assert.False(ParserOptions.Default.AllowSuperCallOutsideConstructor); + + // The option must survive further copies of the options object. + Assert.True((options with { EcmaVersion = EcmaVersion.ES2022 }).AllowSuperCallOutsideConstructor); + } + [Theory] [InlineData("script", "(class { x = await })", EcmaVersion.Latest, null)] [InlineData("module", "(class { x = await })", EcmaVersion.Latest, "Unexpected reserved word")]