Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/Acornima/Parser.State.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down
31 changes: 30 additions & 1 deletion src/Acornima/ParserOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,15 +120,43 @@ public ExperimentalESFeatures ExperimentalESFeatures
/// Gets or sets whether to allow new.target meta-properties in the top-level scope.
/// Defaults to <see langword="false"/>.
/// </summary>
/// <remarks>
/// This option is intended for parsing code which is evaluated in the context of a function
/// (see <see href="https://tc39.es/ecma262/#sec-performeval">PerformEval</see> and its <c>inFunc</c> parameter).
/// Enabling it means that <c>new.target</c> accesses are permitted at the top level of the parsed code.
/// </remarks>
public bool AllowNewTargetOutsideFunction { get => _allowNewTargetOutsideFunction; init => _allowNewTargetOutsideFunction = value; }

internal readonly bool _allowSuperOutsideMethod;
/// <summary>
/// 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 <see langword="false"/>.
/// </summary>
/// <remarks>
/// This option is intended for parsing code which is evaluated in the context of a method
/// (see <see href="https://tc39.es/ecma262/#sec-performeval">PerformEval</see> and its <c>inMethod</c> 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 <c>this</c> 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 <see cref="AllowSuperCallOutsideConstructor"/> for those.
/// </remarks>
public bool AllowSuperOutsideMethod { get => _allowSuperOutsideMethod; init => _allowSuperOutsideMethod = value; }

internal readonly bool _allowSuperCallOutsideConstructor;
/// <summary>
/// Gets or sets whether to allow super calls to appear outside the constructor of a derived class.
/// Defaults to <see langword="false"/>.
/// </summary>
/// <remarks>
/// This option is intended for parsing code which is evaluated in the context of the constructor of a derived class
/// (see <see href="https://tc39.es/ecma262/#sec-performeval">PerformEval</see> and its <c>inDerivedConstructor</c> 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 <c>this</c> 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 <see cref="AllowSuperOutsideMethod"/>.
/// </remarks>
public bool AllowSuperCallOutsideConstructor { get => _allowSuperCallOutsideConstructor; init => _allowSuperCallOutsideConstructor = value; }

internal readonly bool _allowTopLevelUsing;
/// <summary>
/// Gets or sets whether to allow using declarations to appear at the top level of scripts as well.
Expand Down
98 changes: 98 additions & 0 deletions test/Acornima.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SyntaxErrorException>(() => 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")]
Expand Down