src/ast.js
/** Base type of all ASTs. */
export class Node {
/**
Convert to JSON.
@see https://github.com/estree/estree
*/
toJSON() {
const obj = { }
obj.type = this.type
// Sort to make JSON rendering deterministic.
Object.keys(this).sort().forEach(key => { obj[key] = this[key] })
return obj
}
/**
For compatibility with other AST representations,
all Node instances have a 'type' property that is the name of that type.
@see https://github.com/estree/estree
*/
get type() {
return this.constructor.name
}
/** @override */
toString() {
return JSON.stringify(this)
}
}
// Abstracts
/** Line that declares new locals. */
export class Declaration extends Node { }
/** Blocks of code have lines that are Statements or Declarations. */
export class Statement extends Node { }
/**
Code that has a value.
To use one in a statement position, see ExpressionStatement.
*/
export class Expression extends Node { }
/**
Can go in a parameter list or on the left side of an assignment.
*/
export class Pattern extends Node { }
// A complete program source tree.
export class Program extends Node {
constructor(body) {
super()
/** @type {Array<Statement>} */
this.body = body
}
}
// Variables
/**
A JavaScript identifier.
It is assumed that you have called `mangleIdentifier` as appropriate.
See also {@link identifier}.
*/
export class Identifier extends Expression {
constructor(name) {
super()
/** @type {String} */
this.name = name
}
}
/** Single declaration within a {@link VariableDeclaration}. */
export class VariableDeclarator extends Node {
constructor(id, init) {
// TODO:ES6 Optional args
if (init=== undefined)
init = null
super()
/** @type {Pattern} */
this.id = id
/** @type {?Expression} */
this.init = init
}
}
/** Accepted kinds of {@link VariableDeclaration}. */
export const VariableDeclarationKind = new Set(['const', 'let', 'var'])
/**
Declares and optionally initializes many variables.
Must be at least one declaration.
*/
export class VariableDeclaration extends Declaration {
constructor(kind, declarations) {
super()
/** @type {VariableDeclarationKind} */
this.kind = kind
/** @type {Array<VariableDeclarator>} */
this.declarations = declarations
if (this.declarations.length === 0)
throw new Error('VariableDeclaration must have at least 1 declaration.')
}
}
// Statements
/**
An empty statement, i.e., a solitary semicolon.
Not useful for code generation, but some parsers will return these.
*/
export class EmptyStatement extends Statement { }
/** A block statement, i.e., a sequence of statements surrounded by braces. */
export class BlockStatement extends Statement {
constructor(body) {
super()
/** @type {Array<Statement>} */
this.body = body
}
}
/**
An expression statement, i.e., a statement consisting of a single expression.
See `esast.util toStatement toStatements`.
*/
export class ExpressionStatement extends Statement {
constructor(expression) {
super()
/** @type {Expression} */
this.expression = expression
}
}
/** An if (or if ... else) statement. */
export class IfStatement extends Statement {
constructor(test, consequent, alternate) {
// TODO:ES6 Optional arguments
if (alternate === undefined)
alternate = null
super()
/** @type {Expression} */
this.test = test
/** @type {Statement} */
this.consequent = consequent
/** @type {?Statement} */
this.alternate = alternate
}
}
/** A statement prefixed by a label. */
export class LabeledStatement extends Statement {
constructor(label, body) {
super()
/** @type {Identifier} */
this.label = label
/** @type {Statement} */
this.body = body
}
}
export class BreakStatement extends Statement {
/** The `break` keyword. */
constructor(label) {
// TODO:ES6 Optional args
if (label === undefined)
label = null
super()
/** @type {?Identifier} */
this.label = label
}
}
/** The `continue` keyword. */
export class ContinueStatement extends Statement {
constructor(label) {
// TODO:ES6 Optional args
if (label === undefined)
label = null
super()
/** @type {?Identifier} */
this.label = label
}
}
/**
`switch (discriminant) { cases }`
Only the last entry of `cases` is allowed to be `default`.
*/
export class SwitchStatement extends Statement {
constructor(discriminant, cases) {
super()
/** @type {Expression} */
this.discriminant = discriminant
/** @type {Array<SwitchCase>} */
this.cases = cases
}
}
/**
A single `case` within a SwitchStatement.
If `test` is `null`, this is the `default` case.
*/
export class SwitchCase extends Statement {
constructor(test, consequent) {
// TODO:ES6 Optional args
if (test === undefined)
test = null
super()
/** @type {?Expression} */
this.test = test
/** @type {Array<Statement> */
this.consequent = consequent
}
}
/** The `return` keyword, optionally followed by an Expression to return. */
export class ReturnStatement extends Statement {
constructor(argument) {
// TODO:ES6 Optional args
if (argument === undefined)
argument = null
super()
/** @type {?Expression} */
this.argument = argument
}
}
/**
The `throw` keyword, and something to throw.
See `esast.util throwError`.
*/
export class ThrowStatement extends Statement {
constructor(argument) {
super()
/** @type {Expression} */
this.argument = argument
}
}
/**
`try { block } catch (handler.param) { handler.body } finally { finalizer }`
At least one of `handler` or `finalizer` must be non-null.
*/
export class TryStatement extends Statement {
constructor(block, handler, finalizer) {
// TODO:ES6 Optional args
if (handler === undefined)
handler = null
if (finalizer === undefined)
finalizer = null
super()
/** @type {BlockStatement} */
this.block = block
/** @type {?CatchClause} */
this.handler = handler
/** @type {?BlockStatement} */
this.finalizer = finalizer
}
}
/** Must be *part* of a {@link TryStatement} -- does *not* follow it. */
export class CatchClause extends Node {
constructor(param, body) {
super()
/** @type {Pattern} */
this.param = param
/** @type {BlockStatement} */
this.body = body
}
}
/** `while (test) body` */
export class WhileStatement extends Statement {
constructor(test, body) {
super()
/** @type {Expression} */
this.test = test
/** @type {Statement} */
this.body = body
}
}
/** `do body while (test)` */
export class DoWhileStatement extends Statement {
constructor(body, test) {
super()
/** @type {Statement} */
this.body = body
/** @type {Expression} */
this.test = test
}
}
/**
`for (init; test; update) body`
Not to be confused with ForInStatement or ForOfStatement.
*/
export class ForStatement extends Statement {
constructor(init, test, update, body) {
super()
/** @type {?(VariableDeclaration | Expression)} */
this.init = init
/** @type {?Expression} */
this.test = test
/** @type {?Expression} */
this.update = update
/** @type {Statement} */
this.body = body
}
}
/** `for (left in right) body` */
export class ForInStatement extends Statement {
constructor(left, right, body) {
super()
/** @type {VariableDeclaration | Expression} */
this.left = left
/** @type {Expression} */
this.right = right
/** @type {Statement} */
this.body = body
}
}
/** `for (left of right) body` */
export class ForOfStatement extends Statement {
constructor(left, right, body) {
super()
/** @type {VariableDeclaration | Expression} */
this.left = left
/** @type {Expression} */
this.right = right
/** @type {Statement} */
this.body = body
}
}
/** The `debugger` keyword. */
export class DebuggerStatement extends Statement { }
// Declarations
/** FunctionDeclaration | FunctionExpression | ArrowFunctionExpression */
export class FunctionAbstract extends Node { }
class FunctionNonArrow extends FunctionAbstract {
constructor(id, params, body, generator) {
// TODO:ES6 Optional args
if (generator === undefined)
generator = false
super()
/** @type {Identifier} */
this.id = id
/** @type {Array<Pattern>} */
this.params = params
/** @type {BlockStatement} */
this.body = body
/** @type {boolean} */
this.generator = generator
}
}
// TODO: Declaration too
/** {@link Function} in declaration position. */
export class FunctionDeclaration extends FunctionNonArrow { }
// Expressions
export class Literal extends Expression {
constructor(value) {
super()
/** @type {number|string|boolean|null} */
this.value = value
}
}
/** The `this` keyword. */
export class ThisExpression extends Expression { }
/** `[ elements ]` */
export class ArrayExpression extends Expression {
constructor(elements) {
super()
/** @type {Array<?Expression>} */
this.elements = elements
}
}
/** Accepted kinds of {@link Property}. */
export const PropertyKind = new Set(['init', 'get', 'set'])
/**
Part of an ObjectExpression.
If kind is 'get' or 'set', then value should be a FunctionExpression.
*/
export class Property extends Node {
constructor(kind, key, value, method,shorthand, computed) {
// TODO:ES6 Optional args
if (method === undefined)
method = shorthand = computed = false
super()
/** @type {PropertyKind} */
this.kind = kind
/** @type {Literal | Identifier} */
this.key = key
/** @type {Expression} */
this.value = value
/** @type {boolean} */
this.method = method
/** @type {boolean} */
this.shorthand
/** @type {boolean} */
this.computed = computed
if (this.kind !== 'init') {
if (!(this.value instanceof FunctionExpression))
throw new Error('get/set Property\'s value must be a FunctionExpression.')
if (this.value.id !== null)
throw new Error(
'get/set Property\'s value must not have id; ' +
'that is stored in the `key` of the Property.')
if (this.value.generator)
throw new Error('get/set can not be a generator.')
}
}
}
/** An object literal. */
export class ObjectExpression extends Expression {
constructor(properties) {
super()
/** @type {Array<Property>} */
this.properties = properties
}
}
// TODO: Expression too
/** {@link Function} in expression position. */
export class FunctionExpression extends FunctionNonArrow { }
/** Like FunctionExpression but uses the `params => body` form. */
// TODO: extends FunctionAbstract too
export class ArrowFunctionExpression extends Expression {
constructor(params, body) {
super()
/** @type {Array<Pattern>} */
this.params = params
/** @type {BlockStatement | Expression} */
this.body = body
}
}
/**
`expressions[0], expressions[1], ...`
Expression composed of other expressions, separated by the comma operator.
*Not* for parameter lists.
*/
export class SequenceExpression extends Expression {
constructor(expressions) {
super()
/** @type {Array<Expression>} */
this.expressions = expressions
}
}
/** Accepted kinds of {@link UnaryExpression}. */
export const UnaryOperator = new Set(['-', '+', '!', '~', 'typeof', 'void', 'delete'])
/**
`operator argument`
Calls a unary operator.
*/
export class UnaryExpression extends Expression {
constructor(operator, argument) {
super()
/** @type {UnaryOperator} */
this.operator = operator
/** @type {Expression} */
this.argument = argument
}
/** Always true. Needed for comparibility with estree. */
get prefix() {
return true
}
}
/** Accepted kinds of {@link BinaryExpression}. */
export const BinaryOperator = new Set([
'==', '!=', '===', '!==',
'<', '<=', '>', '>=',
'<<', '>>', '>>>',
'+', '-', '*', '/', '%',
'|', '^', '&', 'in',
'instanceof'])
/**
`left operator right`
Calls a binary operator.
*/
export class BinaryExpression extends Expression {
constructor(operator, left, right) {
super()
/** @type {BinaryOperator} */
this.operator = operator
/** @type {Expression} */
this.left = left
/** @type {Expression} */
this.right = right
}
}
/** Accepted kinds of {@link AssignmentExpression}. */
export const AssignmentOperator = new Set([
'=', '+=', '-=', '*=', '/=', '%=',
'<<=', '>>=', '>>>=',
'|=', '^=', '&='
])
/**
`left operator right`
Mutates an existing variable.
Do not confuse with VariableDeclaration.
*/
export class AssignmentExpression extends Expression {
constructor(operator, left, right) {
super()
/** @type {AssignmentOperator} */
this.operator = operator
/** @type {Pattern} */
this.left = left
/** @type {Expression} */
this.right = right
}
}
/** Accepted kinds of {@link UpdateExpression}. */
export const UpdateOperator = new Set(['++', '--'])
/**
`++argument` or `argument++`
Increments or decrements a number.
*/
export class UpdateExpression extends Expression {
constructor(operator, argument, prefix) {
super()
/** @type {UpdateOperator} */
this.operator = operator
/** @type {Expression} */
this.argument = argument
/** @type {boolean} */
this.prefix = prefix
}
}
/** Accepted kinds of {@link LogicalExpression}. */
export const LogicalOperator = new Set(['||', '&&'])
/**
`left operator right`
Calls a lazy logical operator.
*/
export class LogicalExpression extends Expression {
constructor(operator, left, right) {
super()
/** @type {LogicalOperator} */
this.operator = operator
/** @type {Expression} */
this.left = left
/** @type {Expression} */
this.right = right
}
}
/** `test ? consequent : alternate` */
export class ConditionalExpression extends Expression {
constructor(test, consequent, alternate) {
super()
/** @type {Expression} */
this.test = test
/** @type {Expression} */
this.consequent = consequent
/** @type {Expression} */
this.alternate = alternate
}
}
/**
`new callee(arguments)`
Just like {@link CallExpression} but with `new` in front.
*/
export class NewExpression extends Expression {
constructor(callee, _arguments) {
super()
/** @type {Expression} */
this.callee = callee
/* @type {Array<Expression>} */
this.arguments = _arguments
}
}
/** `callee(arguments)` */
export class CallExpression extends Expression {
constructor(callee, _arguments) {
super()
/** @type {Expression} */
this.callee = callee
/* @type {Array<Expression>} */
this.arguments = _arguments
}
}
/** `...args` in a CallExpression. */
export class SpreadElement extends Node {
constructor(argument) {
super()
/** @type {Expression} */
this.argument = argument
}
}
/**
If computed === true, `object[property]`.
Else, `object.property` -- meaning property should be an Identifier.
*/
export class MemberExpression extends Expression {
constructor(object, property) {
super()
/** @type {Expression} */
this.object = object
/** @type {Expression} */
this.property = property
}
/** Needed for compatibility with estree. */
get computed() {
return !(this.property instanceof Identifier)
}
}
/** `yield argument` or `yield* argument` */
export class YieldExpression extends Expression {
constructor(argument, delegate) {
super()
/** @type {?Expression} */
this.argument = argument
/** @type {boolean} */
this.delegate = delegate
if (this.delegate && this.argument === null)
throw new Error('Can not yield* without argument.')
}
}
// Templates
/**
A template with no tag.
It alternates between quasis and expressions.
It should begin and end with quasis, using {@link TemplateElement.empty} if necessary.
This means that `${1}${2}` has 3 empty quasis!
*/
export class TemplateLiteral extends Expression {
constructor(quasis, expressions) {
super()
/** @type {Array<TemplateElement>} */
this.quasis = quasis
/** @type {Array<Expression>} */
this.expressions = expressions
if (this.quasis.length !== this.expressions.length + 1)
throw new Error(
'There must be 1 more quasi than expressions.\n' +
'Maybe you need to add an empty quasi to the front or end.')
}
}
/** Part of a TemplateLiteral. */
export class TemplateElement extends Node {
/**
TemplateElement whose raw source is `str`.
@param {string} str
*/
static forRawString(str) {
return new TemplateElement(false, {
// TODO: A way to calculate this?
cooked: null,
raw: str
})
}
/**
TemplateElement evaluating to `str`.
Uses escape sequences as necessary.
@param {string} str
*/
static forString(str) {
return new TemplateElement(false, {
cooked: str,
raw: escapeStringForTemplate(str)
})
}
/** TemplateElement with empty value. */
static get empty() {
return this.forString('')
}
constructor(tail, value) {
super()
/**
Use this to mark the last TemplateElement.
@type {boolean}
*/
this.tail = tail
/** @type {{cooked: string, raw: string}} */
this.value = value
}
}
const
escapeStringForTemplate = str =>
str.replace(/[{\\`\n\t\b\f\v\r\u2028\u2029]/g, ch => templateEscapes[ch]),
templateEscapes = {
// Need to make sure "${" is escaped.
'{': '\\{',
'`': '\\`',
'\\': '\\\\',
'\n': '\\n',
'\t': '\\t',
'\b': '\\b',
'\f': '\\f',
'\v': '\\v',
'\r': '\\r',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
}
/** TemplateLiteral with a tag in front, like`this`. */
export class TaggedTemplateExpression extends Expression {
constructor(tag, quasi) {
super()
/** @type {Expression} */
this.tag = tag
/** @type {TemplateLiteral} */
this.quasi = quasi
}
}
// Patterns
/**
`{ a, b: c } =`
Object deconstructing pattern.
*/
export class ObjectPattern extends Pattern {
constructor(properties) {
super()
/** @type {Array<AssignmentProperty>} */
this.properties = properties
}
}
/**
Just like a Property, but kind is always `init`.
Although technically its own type, `_.type` will be 'Property'.
*/
export class AssignmentProperty extends Node {
constructor(key, value) {
// TODO:ES6 Optional args
if (value === undefined)
value = key
super()
/** @type {Identifier} */
this.key = key
/** @type {Pattern} */
this.value = value
}
get type() { return 'Property' }
get kind() { return 'init' }
get method() { return false }
get shorthand() { return true }
get computed() { return false }
}
/**
`[ a, b ] = ...`.
Array deconstructing pattern.
*/
export class ArrayPattern extends Pattern {
constructor(elements) {
super()
/** @type {Array<?Pattern>} */
this.elements = elements
}
}
/**
Can be the last argument to a FunctionExpression/FunctionDeclaration
or go at the end of an ArrayPattern.
*/
export class RestElement extends Pattern {
constructor(argument) {
super()
/** @type {Pattern} */
this.argument = argument
}
}
// Classes
/** Accepted kinds of {@link MethodDefinition}. */
export const MethodDefinitionKind = new Set(['constructor', 'method', 'get', 'set'])
/** Part of a {@link ClassBody}. */
export class MethodDefinition extends Node {
/** @param {FunctionExpression} value */
static constructor(value) {
return new MethodDefinition(new Identifier('constructor'), value, 'constructor')
}
constructor(key, value, kind, _static, computed) {
// TODO:ES6 Optional args
if (_static === undefined)
_static = false
if (computed === undefined)
computed = false
if (kind === 'constructor' && !(
key instanceof Identifier && key.name === 'constructor' && !_static && !computed))
throw new Error(
'Constructor method should created with `MethodDefinition.constructor`.')
super()
/** @type {Identifier | Literal} */
this.key = key
/** @type {FunctionExpression} */
this.value = value
/** @type {MethodDefinitionKind} */
this.kind = kind
/** @type {boolean} */
this.static = _static
/** @type {boolean} */
this.computed = computed
if (value.id !== null)
throw new Error(
'MethodDefinition value should not have id; that is handled by `key`.')
}
}
/** Contents of a {@link Class}. */
export class ClassBody extends Node {
constructor(body) {
super()
/** @type {Array<MethodDefinition>} */
this.body = body
}
}
/** {@link ClassDeclaration} | {@link ClassExpression} */
export class Class extends Node { }
// TODO: extends Declaration too
/** {@link Class} in declaration position. */
export class ClassDeclaration extends Class {
constructor(id, superClass, body) {
super()
/** @type {Identifier} */
this.id = id
/** @type {?Expression} */
this.superClass = superClass
/** @type {ClassBody} */
this.body = body
}
}
/** {@link Class} in expression position. */
export class ClassExpression extends Class {
constructor(id, superClass, body) {
super()
/** @type {?Identifier} */
this.id = id
/** @type {?Expression} */
this.superClass = superClass
/** @type {ClassBody} */
this.body = body
}
}
// Modules
/** A specifier in an import or export declaration. */
export class ModuleSpecifier extends Node { }
/**
{@link ImportSpecifier} | {@link ImportDefaultSpecifier} | {@link ImportNamespaceSpecifier}
*/
export class ImportSpecifierAbstract extends Node { }
/**
`import specifiers from source`
Only one specifier may be a ImportDefaultSpecifier.
If there is an ImportNamespaceSpecifier, it must be the only specifier.
*/
export class ImportDeclaration extends Node {
constructor(specifiers, source) {
super()
/** @type {Array<ImportSpecifierAbstract>} */
this.specifiers = specifiers
/** @type {Literal<string>} */
this.source = source
}
}
/**
A non-default import. Used in an ImportDeclaration.
For `import { a } from "source"`, just pass one argument and local will = imported.
For `import { a as b } from "source"`, make imported `a` and local `b`.
*/
export class ImportSpecifier extends ModuleSpecifier {
constructor(imported, local) {
// TODO:ES6 Optional args
if (local === undefined)
local = imported
super()
/** @type {Identifier} */
this.imported = imported
/** @type {Identifier} */
this.local = local
}
}
/** The default export, as in `import a from "source"`. */
export class ImportDefaultSpecifier extends ImportSpecifierAbstract {
constructor(local) {
super()
/** @type {Identifier} */
this.local = local
}
}
/** Object of every export, as in `import * as a from "source"`. */
export class ImportNamespaceSpecifier extends ImportSpecifierAbstract {
constructor(local) {
super()
/** @type {Identifier} */
this.local = local
}
}
/**
A non-default export. Used in an ExportNamedDeclaration.
For `export { a } from "source"`, just pass one argument local will = exported.
For `export { a as b }`, make exported `b` and local `a`.
*/
export class ExportSpecifier extends ModuleSpecifier {
constructor(exported, local) {
// TODO:ES6 Optional args
if (local === undefined)
local = exported
super()
/** @type {Identifier} */
this.exported = exported
/** @type {Identifier} */
this.local = local
}
}
/**
Exports multiple values as in `export { a, b as c }`.
If source !== null,
re-exports from that module as in `export { ... } from "source"`.
*/
export class ExportNamedDeclaration extends Node {
constructor(declaration, specifiers, source) {
// TODO:ES6 Optional arguments
if (specifiers === undefined)
specifiers = []
if (source === undefined)
source = null
super()
/** @type {?Declaration} */
this.declaration = declaration
/** @type {Array<ExportSpecifier>} */
this.specifiers = specifiers
/** @type {?Literal<string>} */
this.source = source
if (declaration !== null && !(specifiers.length === 0 && source === null))
throw new Error('Declaration can not be combined with specifiers/source.')
}
}
/** `export default declaration` */
export class ExportDefaultDeclaration extends Node {
constructor(declaration) {
super()
/** @type {Declaration | Expression} */
this.declaration = declaration
}
}
/** `export * from source` */
export class ExportAllDeclaration extends Node {
constructor(source) {
super()
/** @type {Literal<string>} */
this.source = source
}
}