Skip to content

Commit

Permalink
fix a panic with "export default interface\n"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Nov 23, 2023
1 parent a8313d2 commit 7baefdb
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 9 deletions.
37 changes: 28 additions & 9 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6700,6 +6700,7 @@ type parseStmtOpts struct {
isModuleScope bool
isNamespaceScope bool
isExport bool
isExportDefault bool
isNameOptional bool // For "export default" pseudo-statements
isTypeScriptDeclare bool
isForLoopInit bool
Expand Down Expand Up @@ -6839,6 +6840,8 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
return defaultName
}

// "export default async function() {}"
// "export default async function foo() {}"
if p.lexer.IsContextualKeyword("async") {
asyncRange := p.lexer.Range()
p.lexer.Next()
Expand Down Expand Up @@ -6872,20 +6875,27 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
DefaultName: defaultName, Value: js_ast.Stmt{Loc: loc, Data: &js_ast.SExpr{Value: expr}}}}
}

if p.lexer.Token == js_lexer.TFunction || p.lexer.Token == js_lexer.TClass || p.lexer.IsContextualKeyword("interface") {
// "export default class {}"
// "export default class Foo {}"
// "export default function() {}"
// "export default function foo() {}"
// "export default interface Foo {}"
// "export default interface + 1"
if p.lexer.Token == js_lexer.TFunction || p.lexer.Token == js_lexer.TClass ||
(p.options.ts.Parse && p.lexer.IsContextualKeyword("interface")) {
stmt := p.parseStmt(parseStmtOpts{
deferredDecorators: opts.deferredDecorators,
isNameOptional: true,
isExportDefault: true,
lexicalDecl: lexicalDeclAllowAll,
hasNoSideEffectsComment: opts.hasNoSideEffectsComment,
})
if _, ok := stmt.Data.(*js_ast.STypeScript); ok {
return stmt // This was just a type annotation
}

// Use the statement name if present, since it's a better name
var defaultName ast.LocRef
switch s := stmt.Data.(type) {
case *js_ast.STypeScript, *js_ast.SExpr:
return stmt // Handle the "interface" case above
case *js_ast.SFunction:
if s.Fn.Name != nil {
defaultName = ast.LocRef{Loc: defaultLoc, Ref: s.Fn.Name.Ref}
Expand All @@ -6901,7 +6911,6 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
default:
panic("Internal error")
}

return js_ast.Stmt{Loc: loc, Data: &js_ast.SExportDefault{DefaultName: defaultName, Value: stmt}}
}

Expand All @@ -6910,6 +6919,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
expr := p.parseExpr(js_ast.LComma)

// "export default abstract class {}"
// "export default abstract class Foo {}"
if p.options.ts.Parse && isIdentifier && name == "abstract" && !p.lexer.HasNewlineBefore {
if _, ok := expr.Data.(*js_ast.EIdentifier); ok && (p.lexer.Token == js_lexer.TClass || opts.deferredDecorators != nil) {
stmt := p.parseClassStmt(loc, parseStmtOpts{
Expand Down Expand Up @@ -7741,18 +7751,18 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {

default:
isIdentifier := p.lexer.Token == js_lexer.TIdentifier
nameRange := p.lexer.Range()
name := p.lexer.Identifier.String

// Parse either an async function, an async expression, or a normal expression
var expr js_ast.Expr
if isIdentifier && p.lexer.Raw() == "async" {
asyncRange := p.lexer.Range()
p.lexer.Next()
if p.lexer.Token == js_lexer.TFunction && !p.lexer.HasNewlineBefore {
p.lexer.Next()
return p.parseFnStmt(asyncRange.Loc, opts, true /* isAsync */, asyncRange)
return p.parseFnStmt(nameRange.Loc, opts, true /* isAsync */, nameRange)
}
expr = p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange, js_ast.LLowest, 0), js_ast.LLowest, nil, 0)
expr = p.parseSuffix(p.parseAsyncPrefixExpr(nameRange, js_ast.LLowest, 0), js_ast.LLowest, nil, 0)
} else {
var stmt js_ast.Stmt
expr, stmt, _ = p.parseExprOrLetOrUsingStmt(opts)
Expand Down Expand Up @@ -7800,11 +7810,20 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {

case "interface":
// "interface Foo {}"
if !p.lexer.HasNewlineBefore {
// "export default interface Foo {}"
// "export default interface \n Foo {}"
if !p.lexer.HasNewlineBefore || opts.isExportDefault {
p.skipTypeScriptInterfaceStmt(parseStmtOpts{isModuleScope: opts.isModuleScope})
return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
}

// "interface \n Foo {}"
// "export interface \n Foo {}"
if opts.isExport {
p.log.AddError(&p.tracker, nameRange, "Unexpected \"interface\"")
panic(js_lexer.LexerPanic{})
}

case "abstract":
if !p.lexer.HasNewlineBefore && (p.lexer.Token == js_lexer.TClass || opts.deferredDecorators != nil) {
return p.parseClassStmt(loc, opts)
Expand Down
8 changes: 8 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,14 @@ func TestExport(t *testing.T) {
expectParseError(t, "export let", "<stdin>: ERROR: Expected identifier but found end of file\n")
expectParseError(t, "export const", "<stdin>: ERROR: Expected identifier but found end of file\n")

// Do not parse TypeScript export syntax in JavaScript
expectParseError(t, "export enum Foo {}", "<stdin>: ERROR: Unexpected \"enum\"\n")
expectParseError(t, "export interface Foo {}", "<stdin>: ERROR: Unexpected \"interface\"\n")
expectParseError(t, "export namespace Foo {}", "<stdin>: ERROR: Unexpected \"namespace\"\n")
expectParseError(t, "export abstract class Foo {}", "<stdin>: ERROR: Unexpected \"abstract\"\n")
expectParseError(t, "export declare class Foo {}", "<stdin>: ERROR: Unexpected \"declare\"\n")
expectParseError(t, "export declare function foo() {}", "<stdin>: ERROR: Unexpected \"declare\"\n")

// String export alias with "export {}"
expectPrinted(t, "let x; export {x as ''}", "let x;\nexport { x as \"\" };\n")
expectPrinted(t, "let x; export {x as '🍕'}", "let x;\nexport { x as \"🍕\" };\n")
Expand Down
12 changes: 12 additions & 0 deletions internal/js_parser/ts_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,18 @@ func TestTSInterface(t *testing.T) {
expectPrintedTS(t, "interface A<T extends number> extends B.C<D, E>, F.G<H, I> {} x", "x;\n")
expectPrintedTS(t, "export interface A<T extends number> extends B.C<D, E>, F.G<H, I> {} x", "x;\n")
expectPrintedTS(t, "export default interface Foo {} x", "x;\n")
expectParseErrorTS(t, "export default interface + x",
"<stdin>: ERROR: \"interface\" is a reserved word and cannot be used in an ECMAScript module\n"+
"<stdin>: NOTE: This file is considered to be an ECMAScript module because of the \"export\" keyword here:\n")

// Check ASI for "interface"
expectPrintedTS(t, "interface\nFoo\n{}", "interface;\nFoo;\n{\n}\n")
expectPrintedTS(t, "export default interface\nFoo {} x", "x;\n")
expectPrintedTS(t, "export default interface\nFoo\n{} x", "x;\n")
expectParseErrorTS(t, "interface\nFoo {}", "<stdin>: ERROR: Expected \";\" but found \"{\"\n")
expectParseErrorTS(t, "export interface\nFoo {}", "<stdin>: ERROR: Unexpected \"interface\"\n")
expectParseErrorTS(t, "export interface\nFoo\n{}", "<stdin>: ERROR: Unexpected \"interface\"\n")
expectParseErrorTS(t, "export default interface\nFoo", "<stdin>: ERROR: Expected \"{\" but found end of file\n")
}

func TestTSNamespace(t *testing.T) {
Expand Down

0 comments on commit 7baefdb

Please sign in to comment.