Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parsing ExportDefaultSpecifier and ExportNamespaceSpecifier #541

Closed
Closed
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
83 changes: 73 additions & 10 deletions src/loose/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ lp.parseStatement = function() {
return this.parseImport()

case tt._export:
return this.parseExport()
return this.parseExport(node)

default:
if (this.toks.isAsyncFunction()) {
Expand Down Expand Up @@ -334,14 +334,51 @@ lp.parseFunction = function(node, isStatement, isAsync) {
return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression")
}

lp.parseExport = function() {
let node = this.startNode()
lp.parseExport = function(node) {
this.next()
if (this.eat(tt.star)) {
node.source = this.eatContextual("from") ? this.parseExprAtom() : this.dummyString()
return this.finishNode(node, "ExportAllDeclaration")
}
if (this.eat(tt._default)) {
if (this.tok.type === tt.star) {
const specifier = this.startNode()
this.next()
if (this.options.exportExtensions &&
this.eatContextual("as")) {
// export * as ns from '...'
specifier.exported = this.parseIdent()
node.specifiers = [
this.finishNode(specifier, "ExportNamespaceSpecifier")
]
this.parseExportSpecifiersMaybe(node)
this.parseExportFrom(node)
} else {
// export * from '...'
this.parseExportFrom(node)
return this.finishNode(node, "ExportAllDeclaration")
}
} else if (this.options.exportExtensions &&
this.isExportDefaultSpecifier()) {
// export def from '...'
const specifier = this.startNode()
specifier.exported = this.parseIdent(true)
node.specifiers = [
this.finishNode(specifier, "ExportDefaultSpecifier")
]
if (this.tok.type === tt.comma &&
this.lookAhead(1).type === tt.star) {
// export def, * as ns from '...'
this.expect(tt.comma)
const specifier = this.startNode()
this.expect(tt.star)
specifier.exported = this.eatContextual("as")
? this.parseIdent()
: this.dummyIdent()
node.specifiers.push(
this.finishNode(specifier, "ExportNamespaceSpecifier")
)
} else {
// export def, { x, y as z } from '...'
this.parseExportSpecifiersMaybe(node)
}
this.parseExportFrom(node)
} else if (this.eat(tt._default)) {
// export default (function foo() {}) // This is FunctionExpression.
let isAsync
if (this.tok.type === tt._function || (isAsync = this.toks.isAsyncFunction())) {
Expand All @@ -356,8 +393,9 @@ lp.parseExport = function() {
this.semicolon()
}
return this.finishNode(node, "ExportDefaultDeclaration")
}
if (this.tok.type.keyword || this.toks.isLet() || this.toks.isAsyncFunction()) {
} else if (this.tok.type.keyword ||
this.toks.isLet() ||
this.toks.isAsyncFunction()) {
node.declaration = this.parseStatement()
node.specifiers = []
node.source = null
Expand All @@ -370,6 +408,31 @@ lp.parseExport = function() {
return this.finishNode(node, "ExportNamedDeclaration")
}

lp.isExportDefaultSpecifier = function() {
if (this.tok.type !== tt.name) {
return false
}

const lookAhead = this.lookAhead(1)
return lookAhead.type === tt.comma ||
(lookAhead.type === tt.name &&
lookAhead.value === "from")
}

lp.parseExportSpecifiersMaybe = function(node) {
if (this.eat(tt.comma)) {
node.specifiers.push.apply(
node.specifiers,
this.parseExportSpecifierList()
)
}
}

lp.parseExportFrom = function(node) {
const hasSource = this.eatContextual("from") && this.tok.type === tt.string
node.source = hasSource ? this.parseExprAtom() : this.dummyString()
}

lp.parseImport = function() {
let node = this.startNode()
this.next()
Expand Down
3 changes: 3 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const defaultOptions = {
// When enabled, hashbang directive in the beginning of file
// is allowed and treated as a line comment.
allowHashBang: false,
// When true, `export * as ns from "..."` and `export def from "..."`
// export extensions will be enabled.
exportExtensions: false,
// When `locations` is on, `loc` properties holding objects with
// `start` and `end` properties in `{line, column}` form (with
// line being 1-based and column 0-based) will be attached to the
Expand Down
2 changes: 1 addition & 1 deletion src/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {has} from "./util"
const pp = Parser.prototype

// Object.assign polyfill
const assign = Object.assign || function(target, ...sources) {
export const assign = Object.assign || function(target, ...sources) {
for (let i = 0; i < sources.length; i++) {
const source = sources[i]
for (const key in source) {
Expand Down
3 changes: 3 additions & 0 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class Parser {
this.inModule = options.sourceType === "module"
this.strict = this.inModule || this.strictDirective(this.pos)

// Temporarily true while Parser#lookAhead calls this.next().
this.isLookAhead = false

// Used to signify the start of a potential arrow function
this.potentialArrowAt = -1

Expand Down
122 changes: 102 additions & 20 deletions src/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,14 +590,49 @@ pp.parseClassSuper = function(node) {

pp.parseExport = function(node, exports) {
this.next()
// export * from '...'
if (this.eat(tt.star)) {
this.expectContextual("from")
node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected()
this.semicolon()
return this.finishNode(node, "ExportAllDeclaration")
}
if (this.eat(tt._default)) { // export default ...
if (this.type === tt.star) {
const specifier = this.startNode()
this.next()
if (this.options.exportExtensions &&
this.eatContextual("as")) {
// export * as ns from '...'
specifier.exported = this.parseIdent(true)
node.specifiers = [
this.finishNode(specifier, "ExportNamespaceSpecifier")
]
this.parseExportSpecifiersMaybe(node)
this.parseExportFromWithCheck(node, exports, true)
} else {
// export * from '...'
this.parseExportFromWithCheck(node, exports, true)
return this.finishNode(node, "ExportAllDeclaration")
}
} else if (this.options.exportExtensions &&
this.isExportDefaultSpecifier()) {
// export def from '...'
const specifier = this.startNode()
specifier.exported = this.parseIdent(true)
node.specifiers = [
this.finishNode(specifier, "ExportDefaultSpecifier")
]
if (this.type === tt.comma &&
this.lookAhead(1).type === tt.star) {
// export def, * as ns from '...'
this.expect(tt.comma)
const specifier = this.startNode()
this.expect(tt.star)
this.expectContextual("as")
specifier.exported = this.parseIdent(true)
node.specifiers.push(
this.finishNode(specifier, "ExportNamespaceSpecifier")
)
} else {
// export def, { x, y as z } from '...'
this.parseExportSpecifiersMaybe(node)
}
this.parseExportFromWithCheck(node, exports, true)
} else if (this.eat(tt._default)) {
// export default ...
this.checkExport(exports, "default", this.lastTokStart)
let isAsync
if (this.type === tt._function || (isAsync = this.isAsyncFunction())) {
Expand All @@ -613,34 +648,81 @@ pp.parseExport = function(node, exports) {
this.semicolon()
}
return this.finishNode(node, "ExportDefaultDeclaration")
}
// export var|const|let|function|class ...
if (this.shouldParseExportStatement()) {
} else if (this.shouldParseExportDeclaration()) {
// export var|const|let|function|class ...
node.declaration = this.parseStatement(true)
if (node.declaration.type === "VariableDeclaration")
this.checkVariableExport(exports, node.declaration.declarations)
else
this.checkExport(exports, node.declaration.id.name, node.declaration.id.start)
node.specifiers = []
node.source = null
} else { // export { x, y as z } [from '...']
} else {
// export { x, y as z } [from '...']
node.declaration = null
node.specifiers = this.parseExportSpecifiers(exports)
if (this.eatContextual("from")) {
node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected()
} else {
this.parseExportFrom(node, false)
}
return this.finishNode(node, "ExportNamedDeclaration")
}

pp.isExportDefaultSpecifier = function() {
if (this.type !== tt.name) {
return false
}

const lookAhead = this.lookAhead(1)
return lookAhead.type === tt.comma ||
(lookAhead.type === tt.name &&
lookAhead.value === "from")
}

pp.parseExportSpecifiersMaybe = function(node) {
if (this.eat(tt.comma)) {
node.specifiers.push.apply(
node.specifiers,
this.parseExportSpecifiers()
)
}
}

pp.parseExportFromWithCheck = function(node, exports, expect) {
this.parseExportFrom(node, expect)

if (node.specifiers) {
for (let i = 0; i < node.specifiers.length; i++) {
const s = node.specifiers[i]
const exported = s.exported
this.checkExport(exports, exported.name, exported.start)
}
}
}

pp.parseExportFrom = function(node, expect) {
if (this.eatContextual("from")) {
node.source = this.type === tt.string
? this.parseExprAtom()
: this.unexpected()
} else {
if (node.specifiers) {
// check for keywords used as local names
for (let i = 0; i < node.specifiers.length; i++) {
if (this.keywords.test(node.specifiers[i].local.name) || this.reservedWords.test(node.specifiers[i].local.name)) {
this.unexpected(node.specifiers[i].local.start)
const local = node.specifiers[i].local
if (local && (this.keywords.test(local.name) ||
this.reservedWords.test(local.name))) {
this.unexpected(local.start)
}
}
}

if (expect) {
this.unexpected()
} else {
node.source = null
}
this.semicolon()
}
return this.finishNode(node, "ExportNamedDeclaration")

this.semicolon()
}

pp.checkExport = function(exports, name, pos) {
Expand Down Expand Up @@ -674,7 +756,7 @@ pp.checkVariableExport = function(exports, decls) {
this.checkPatternExport(exports, decls[i].id)
}

pp.shouldParseExportStatement = function() {
pp.shouldParseExportDeclaration = function() {
return this.type.keyword === "var" ||
this.type.keyword === "const" ||
this.type.keyword === "class" ||
Expand Down
25 changes: 22 additions & 3 deletions src/tokenize.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {types as tt, keywords as keywordTypes} from "./tokentype"
import {Parser} from "./state"
import {SourceLocation} from "./locutil"
import {lineBreak, lineBreakG, isNewLine, nonASCIIwhitespace} from "./whitespace"
import {assign} from "./scope"
import {create} from "./walk/index"

// Object type used to represent tokens. Note that normally, tokens
// simply exist as properties on the parser object. This is only
Expand Down Expand Up @@ -31,8 +33,9 @@ const isRhino = typeof Packages == "object" && Object.prototype.toString.call(Pa
// Move to the next token

pp.next = function() {
if (this.options.onToken)
if (!this.isLookAhead && this.options.onToken) {
this.options.onToken(new Token(this))
}

this.lastTokEnd = this.end
this.lastTokStart = this.start
Expand Down Expand Up @@ -67,6 +70,20 @@ pp.curContext = function() {
return this.context[this.context.length - 1]
}

// Return a snapshot of the parser after calling this.nextToken(), without
// permanently updating the parser's state.

pp.lookAhead = function(n = 1) {
const old = assign(create(null), this)
const oldIsLookAhead = this.isLookAhead
this.isLookAhead = true
while (n-- > 0) this.nextToken()
this.isLookAhead = oldIsLookAhead
const copy = assign(create(null), this)
assign(this, old)
return copy
}

// Read a single token, updating the parser object's token-related
// properties.

Expand Down Expand Up @@ -111,9 +128,10 @@ pp.skipBlockComment = function() {
this.lineStart = match.index + match[0].length
}
}
if (this.options.onComment)
if (!this.isLookAhead && this.options.onComment) {
this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos,
startLoc, this.curPosition())
}
}

pp.skipLineComment = function(startSkip) {
Expand All @@ -124,9 +142,10 @@ pp.skipLineComment = function(startSkip) {
++this.pos
ch = this.input.charCodeAt(this.pos)
}
if (this.options.onComment)
if (!this.isLookAhead && this.options.onComment) {
this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos,
startLoc, this.curPosition())
}
}

// Called at the start of the parse and after every token. Skips
Expand Down
2 changes: 1 addition & 1 deletion src/walk/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function findNodeBefore(node, pos, test, base, state) {
}

// Fallback to an Object.create polyfill for older environments.
const create = Object.create || function(proto) {
export const create = Object.create || function(proto) {
function Ctor() {}
Ctor.prototype = proto
return new Ctor
Expand Down
Loading