Merge branch 'master' into feat-optional-chaining

This commit is contained in:
Sven SAULEAU
2017-05-29 18:43:40 +02:00
522 changed files with 19051 additions and 5981 deletions

View File

@@ -1,3 +1,6 @@
// @flow
import type { Options } from "./options";
import Parser, { plugins } from "./parser";
import "./parser/util";
import "./parser/statement";
@@ -11,6 +14,8 @@ import { types as tokTypes } from "./tokenizer/types";
import "./tokenizer";
import "./tokenizer/context";
import type { Expression, File } from "./types";
import estreePlugin from "./plugins/estree";
import flowPlugin from "./plugins/flow";
import jsxPlugin from "./plugins/jsx";
@@ -18,12 +23,12 @@ plugins.estree = estreePlugin;
plugins.flow = flowPlugin;
plugins.jsx = jsxPlugin;
export function parse(input, options) {
return new Parser(options, input).parse();
export function parse(input: string, options?: Options): File {
return getParser(options, input).parse();
}
export function parseExpression(input, options) {
const parser = new Parser(options, input);
export function parseExpression(input: string, options?: Options): Expression {
const parser = getParser(options, input);
if (parser.options.strictMode) {
parser.state.strict = true;
}
@@ -32,3 +37,39 @@ export function parseExpression(input, options) {
export { tokTypes };
function getParser(options: ?Options, input: string): Parser {
const cls = options && options.plugins ? getParserClass(options.plugins) : Parser;
return new cls(options, input);
}
const parserClassCache: { [key: string]: Class<Parser> } = {};
/** Get a Parser class with plugins applied. */
function getParserClass(pluginsFromOptions: $ReadOnlyArray<string>): Class<Parser> {
// Filter out just the plugins that have an actual mixin associated with them.
let pluginList = pluginsFromOptions.filter((p) => p === "estree" || p === "flow" || p === "jsx");
if (pluginList.indexOf("flow") >= 0) {
// ensure flow plugin loads last
pluginList = pluginList.filter((plugin) => plugin !== "flow");
pluginList.push("flow");
}
if (pluginList.indexOf("estree") >= 0) {
// ensure estree plugin loads first
pluginList = pluginList.filter((plugin) => plugin !== "estree");
pluginList.unshift("estree");
}
const key = pluginList.join("/");
let cls = parserClassCache[key];
if (!cls) {
cls = Parser;
for (const plugin of pluginList) {
cls = plugins[plugin](cls);
}
parserClassCache[key] = cls;
}
return cls;
}

View File

@@ -1,17 +1,21 @@
// @flow
// A second optional argument can be given to further configure
// the parser process. These options are recognized:
export const defaultOptions: {
sourceType: string,
sourceFilename: any,
startLine: number,
allowReturnOutsideFunction: boolean,
allowImportExportEverywhere: boolean,
allowSuperOutsideMethod: boolean,
plugins: Array<string>,
strictMode: any,
ranges: boolean,
} = {
export type Options = {
sourceType: "script" | "module";
sourceFilename?: string;
startLine: number;
allowReturnOutsideFunction: boolean;
allowImportExportEverywhere: boolean;
allowSuperOutsideMethod: boolean;
plugins: $ReadOnlyArray<string>;
strictMode: ?boolean;
ranges: boolean;
};
export const defaultOptions: Options = {
// Source type ("script" or "module") for different semantics
sourceType: "script",
// Source filename.
@@ -44,8 +48,8 @@ export const defaultOptions: {
// Interpret and default an options object
export function getOptions(opts?: Object): Object {
const options = {};
export function getOptions(opts: ?Options): Options {
const options: any = {};
for (const key in defaultOptions) {
options[key] = opts && key in opts ? opts[key] : defaultOptions[key];
}

31
src/parser/base.js Normal file
View File

@@ -0,0 +1,31 @@
// @flow
import type { Options } from "../options";
import { reservedWords } from "../util/identifier";
import type State from "../tokenizer/state";
export default class BaseParser {
// Properties set by constructor in index.js
options: Options;
inModule: boolean;
plugins: { [key: string]: boolean };
filename: ?string;
// Initialized by Tokenizer
state: State;
input: string;
isReservedWord(word: string): boolean {
if (word === "await") {
return this.inModule;
} else {
return reservedWords[6](word);
}
}
hasPlugin(name: string): boolean {
return !!this.plugins[name];
}
}

View File

@@ -1,5 +1,7 @@
/* eslint max-len: 0 */
// @flow
/**
* Based on the comment attachment algorithm used in espree and estraverse.
*
@@ -24,133 +26,163 @@
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import Parser from "./index";
import BaseParser from "./base";
import type { Comment, Node } from "../types";
function last(stack) {
function last<T>(stack: $ReadOnlyArray<T>): T {
return stack[stack.length - 1];
}
const pp = Parser.prototype;
pp.addComment = function (comment) {
if (this.filename) comment.loc.filename = this.filename;
this.state.trailingComments.push(comment);
this.state.leadingComments.push(comment);
};
pp.processComment = function (node) {
if (node.type === "Program" && node.body.length > 0) return;
const stack = this.state.commentStack;
let lastChild, trailingComments, i, j;
if (this.state.trailingComments.length > 0) {
// If the first comment in trailingComments comes after the
// current node, then we're good - all comments in the array will
// come after the node and so it's safe to add them as official
// trailingComments.
if (this.state.trailingComments[0].start >= node.end) {
trailingComments = this.state.trailingComments;
this.state.trailingComments = [];
} else {
// Otherwise, if the first comment doesn't come after the
// current node, that means we have a mix of leading and trailing
// comments in the array and that leadingComments contains the
// same items as trailingComments. Reset trailingComments to
// zero items and we'll handle this by evaluating leadingComments
// later.
this.state.trailingComments.length = 0;
}
} else {
const lastInStack = last(stack);
if (stack.length > 0 && lastInStack.trailingComments && lastInStack.trailingComments[0].start >= node.end) {
trailingComments = lastInStack.trailingComments;
lastInStack.trailingComments = null;
}
export default class CommentsParser extends BaseParser {
addComment(comment: Comment): void {
if (this.filename) comment.loc.filename = this.filename;
this.state.trailingComments.push(comment);
this.state.leadingComments.push(comment);
}
// Eating the stack.
while (stack.length > 0 && last(stack).start >= node.start) {
lastChild = stack.pop();
}
processComment(node: Node): void {
if (node.type === "Program" && node.body.length > 0) return;
if (lastChild) {
if (lastChild.leadingComments) {
if (lastChild !== node && last(lastChild.leadingComments).end <= node.start) {
node.leadingComments = lastChild.leadingComments;
lastChild.leadingComments = null;
const stack = this.state.commentStack;
let firstChild, lastChild, trailingComments, i, j;
if (this.state.trailingComments.length > 0) {
// If the first comment in trailingComments comes after the
// current node, then we're good - all comments in the array will
// come after the node and so it's safe to add them as official
// trailingComments.
if (this.state.trailingComments[0].start >= node.end) {
trailingComments = this.state.trailingComments;
this.state.trailingComments = [];
} else {
// A leading comment for an anonymous class had been stolen by its first ClassMethod,
// so this takes back the leading comment.
// See also: https://github.com/eslint/espree/issues/158
for (i = lastChild.leadingComments.length - 2; i >= 0; --i) {
if (lastChild.leadingComments[i].end <= node.start) {
node.leadingComments = lastChild.leadingComments.splice(0, i + 1);
// Otherwise, if the first comment doesn't come after the
// current node, that means we have a mix of leading and trailing
// comments in the array and that leadingComments contains the
// same items as trailingComments. Reset trailingComments to
// zero items and we'll handle this by evaluating leadingComments
// later.
this.state.trailingComments.length = 0;
}
} else {
const lastInStack = last(stack);
if (stack.length > 0 && lastInStack.trailingComments && lastInStack.trailingComments[0].start >= node.end) {
trailingComments = lastInStack.trailingComments;
lastInStack.trailingComments = null;
}
}
// Eating the stack.
if (stack.length > 0 && last(stack).start >= node.start) {
firstChild = stack.pop();
}
while (stack.length > 0 && last(stack).start >= node.start) {
lastChild = stack.pop();
}
if (!lastChild && firstChild) lastChild = firstChild;
// Attach comments that follow a trailing comma on the last
// property in an object literal or a trailing comma in function arguments
// as trailing comments
if (firstChild &&
(firstChild.type === "ObjectProperty" ||
(node.type === "CallExpression")) &&
this.state.leadingComments.length > 0) {
const lastComment = last(this.state.leadingComments);
if (lastComment.start >= node.start) {
if (this.state.commentPreviousNode) {
for (j = 0; j < this.state.leadingComments.length; j++) {
if (this.state.leadingComments[j].end < this.state.commentPreviousNode.end) {
this.state.leadingComments.splice(j, 1);
j--;
}
}
if (this.state.leadingComments.length > 0) {
firstChild.trailingComments = this.state.leadingComments;
this.state.leadingComments = [];
}
}
}
}
if (lastChild) {
if (lastChild.leadingComments) {
if (lastChild !== node && last(lastChild.leadingComments).end <= node.start) {
node.leadingComments = lastChild.leadingComments;
lastChild.leadingComments = null;
} else {
// A leading comment for an anonymous class had been stolen by its first ClassMethod,
// so this takes back the leading comment.
// See also: https://github.com/eslint/espree/issues/158
for (i = lastChild.leadingComments.length - 2; i >= 0; --i) {
if (lastChild.leadingComments[i].end <= node.start) {
node.leadingComments = lastChild.leadingComments.splice(0, i + 1);
break;
}
}
}
}
} else if (this.state.leadingComments.length > 0) {
if (last(this.state.leadingComments).end <= node.start) {
if (this.state.commentPreviousNode) {
for (j = 0; j < this.state.leadingComments.length; j++) {
if (this.state.leadingComments[j].end < this.state.commentPreviousNode.end) {
this.state.leadingComments.splice(j, 1);
j--;
}
}
}
if (this.state.leadingComments.length > 0) {
node.leadingComments = this.state.leadingComments;
this.state.leadingComments = [];
}
} else {
// https://github.com/eslint/espree/issues/2
//
// In special cases, such as return (without a value) and
// debugger, all comments will end up as leadingComments and
// will otherwise be eliminated. This step runs when the
// commentStack is empty and there are comments left
// in leadingComments.
//
// This loop figures out the stopping point between the actual
// leading and trailing comments by finding the location of the
// first comment that comes after the given node.
for (i = 0; i < this.state.leadingComments.length; i++) {
if (this.state.leadingComments[i].end > node.start) {
break;
}
}
}
}
} else if (this.state.leadingComments.length > 0) {
if (last(this.state.leadingComments).end <= node.start) {
if (this.state.commentPreviousNode) {
for (j = 0; j < this.state.leadingComments.length; j++) {
if (this.state.leadingComments[j].end < this.state.commentPreviousNode.end) {
this.state.leadingComments.splice(j, 1);
j--;
}
}
}
if (this.state.leadingComments.length > 0) {
node.leadingComments = this.state.leadingComments;
this.state.leadingComments = [];
}
} else {
// https://github.com/eslint/espree/issues/2
//
// In special cases, such as return (without a value) and
// debugger, all comments will end up as leadingComments and
// will otherwise be eliminated. This step runs when the
// commentStack is empty and there are comments left
// in leadingComments.
//
// This loop figures out the stopping point between the actual
// leading and trailing comments by finding the location of the
// first comment that comes after the given node.
for (i = 0; i < this.state.leadingComments.length; i++) {
if (this.state.leadingComments[i].end > node.start) {
break;
// Split the array based on the location of the first comment
// that comes after the node. Keep in mind that this could
// result in an empty array, and if so, the array must be
// deleted.
const leadingComments = this.state.leadingComments.slice(0, i);
node.leadingComments = leadingComments.length === 0 ? null : leadingComments;
// Similarly, trailing comments are attached later. The variable
// must be reset to null if there are no trailing comments.
trailingComments = this.state.leadingComments.slice(i);
if (trailingComments.length === 0) {
trailingComments = null;
}
}
}
// Split the array based on the location of the first comment
// that comes after the node. Keep in mind that this could
// result in an empty array, and if so, the array must be
// deleted.
node.leadingComments = this.state.leadingComments.slice(0, i);
if ((node.leadingComments: Array<any>).length === 0) {
node.leadingComments = null;
}
this.state.commentPreviousNode = node;
// Similarly, trailing comments are attached later. The variable
// must be reset to null if there are no trailing comments.
trailingComments = this.state.leadingComments.slice(i);
if (trailingComments.length === 0) {
trailingComments = null;
if (trailingComments) {
if (trailingComments.length && trailingComments[0].start >= node.start && last(trailingComments).end <= node.end) {
node.innerComments = trailingComments;
} else {
node.trailingComments = trailingComments;
}
}
stack.push(node);
}
this.state.commentPreviousNode = node;
if (trailingComments) {
if (trailingComments.length && trailingComments[0].start >= node.start && last(trailingComments).end <= node.end) {
node.innerComments = trailingComments;
} else {
node.trailingComments = trailingComments;
}
}
stack.push(node);
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,21 @@
import { reservedWords } from "../util/identifier";
// @flow
import type { Options } from "../options";
import type { File } from "../types";
import { getOptions } from "../options";
import Tokenizer from "../tokenizer";
import StatementParser from "./statement";
export const plugins = {};
export const plugins: { [name: string]: (superClass: Class<Parser>) => Class<Parser> } = {};
export default class Parser extends Tokenizer {
constructor(options: Object, input: string) {
export default class Parser extends StatementParser {
constructor(options: ?Options, input: string) {
options = getOptions(options);
super(options, input);
this.options = options;
this.inModule = this.options.sourceType === "module";
this.input = input;
this.plugins = this.loadPlugins(this.options.plugins);
this.plugins = pluginsMap(this.options.plugins);
this.filename = options.sourceFilename;
// If enabled, skip leading hashbang line.
@@ -21,59 +24,18 @@ export default class Parser extends Tokenizer {
}
}
isReservedWord(word: string): boolean {
if (word === "await") {
return this.inModule;
} else {
return reservedWords[6](word);
}
}
hasPlugin(name: string): boolean {
return !!this.plugins[name];
}
extend(name: string, f: Function) {
this[name] = f(this[name]);
}
loadPlugins(pluginList: Array<string>): { [key: string]: boolean } {
const pluginMap = {};
if (pluginList.indexOf("flow") >= 0) {
// ensure flow plugin loads last
pluginList = pluginList.filter((plugin) => plugin !== "flow");
pluginList.push("flow");
}
if (pluginList.indexOf("estree") >= 0) {
// ensure estree plugin loads first
pluginList = pluginList.filter((plugin) => plugin !== "estree");
pluginList.unshift("estree");
}
for (const name of pluginList) {
if (!pluginMap[name]) {
pluginMap[name] = true;
const plugin = plugins[name];
if (plugin) plugin(this);
}
}
return pluginMap;
}
parse(): {
type: "File",
program: {
type: "Program",
body: Array<Object>
}
} {
parse(): File {
const file = this.startNode();
const program = this.startNode();
this.nextToken();
return this.parseTopLevel(file, program);
}
}
function pluginsMap(pluginList: $ReadOnlyArray<string>): { [key: string]: boolean } {
const pluginMap = {};
for (const name of pluginList) {
pluginMap[name] = true;
}
return pluginMap;
}

View File

@@ -1,7 +1,7 @@
import { getLineInfo } from "../util/location";
import Parser from "./index";
// @flow
const pp = Parser.prototype;
import { getLineInfo } from "../util/location";
import CommentsParser from "./comments";
// This function is used to raise exceptions on parse errors. It
// takes an offset integer (into the current `input`) to indicate
@@ -9,11 +9,14 @@ const pp = Parser.prototype;
// of the error message, and then raises a `SyntaxError` with that
// message.
pp.raise = function (pos, message) {
const loc = getLineInfo(this.input, pos);
message += ` (${loc.line}:${loc.column})`;
const err = new SyntaxError(message);
err.pos = pos;
err.loc = loc;
throw err;
};
export default class LocationParser extends CommentsParser {
raise(pos: number, message: string): empty {
const loc = getLineInfo(this.input, pos);
message += ` (${loc.line}:${loc.column})`;
// $FlowIgnore
const err: SyntaxError & { pos: number, loc: Position } = new SyntaxError(message);
err.pos = pos;
err.loc = loc;
throw err;
}
}

View File

@@ -1,259 +1,291 @@
import { types as tt } from "../tokenizer/types";
import Parser from "./index";
// @flow
const pp = Parser.prototype;
import { types as tt, type TokenType } from "../tokenizer/types";
import type { Decorator, Expression, Identifier, Node, ObjectExpression, ObjectPattern, Pattern, RestElement,
SpreadElement } from "../types";
import type { Pos, Position } from "../util/location";
import { NodeUtils } from "./node";
// Convert existing expression atom to assignable pattern
// if possible.
export default class LValParser extends NodeUtils {
// Forward-declaration: defined in expression.js
+checkReservedWord: (word: string, startLoc: number, checkKeywords: boolean, isBinding: boolean) => void;
+parseIdentifier: (liberal?: boolean) => Identifier;
+parseMaybeAssign: (
noIn?: ?boolean,
refShorthandDefaultPos?: ?Pos,
afterLeftParse?: Function,
refNeedsArrowPos?: ?Pos) => Expression;
+parseObj: <T : ObjectPattern | ObjectExpression>(isPattern: boolean, refShorthandDefaultPos?: ?Pos) => T;
// Forward-declaration: defined in statement.js
+parseDecorator: () => Decorator;
pp.toAssignable = function (node, isBinding, contextDescription) {
if (node) {
switch (node.type) {
case "Identifier":
case "ObjectPattern":
case "ArrayPattern":
case "AssignmentPattern":
break;
// Convert existing expression atom to assignable pattern
// if possible.
case "ObjectExpression":
node.type = "ObjectPattern";
for (const prop of (node.properties: Array<Object>)) {
if (prop.type === "ObjectMethod") {
if (prop.kind === "get" || prop.kind === "set") {
this.raise(prop.key.start, "Object pattern can't contain getter or setter");
toAssignable(node: Node, isBinding: ?boolean, contextDescription: string): Node {
if (node) {
switch (node.type) {
case "Identifier":
case "PrivateName":
case "ObjectPattern":
case "ArrayPattern":
case "AssignmentPattern":
break;
case "ObjectExpression":
node.type = "ObjectPattern";
for (const prop of node.properties) {
if (prop.type === "ObjectMethod") {
if (prop.kind === "get" || prop.kind === "set") {
this.raise(prop.key.start, "Object pattern can't contain getter or setter");
} else {
this.raise(prop.key.start, "Object pattern can't contain methods");
}
} else {
this.raise(prop.key.start, "Object pattern can't contain methods");
this.toAssignable(prop, isBinding, "object destructuring pattern");
}
}
break;
case "ObjectProperty":
this.toAssignable(node.value, isBinding, contextDescription);
break;
case "SpreadElement":
node.type = "RestElement";
const arg = node.argument;
this.toAssignable(arg, isBinding, contextDescription);
break;
case "ArrayExpression":
node.type = "ArrayPattern";
this.toAssignableList(node.elements, isBinding, contextDescription);
break;
case "AssignmentExpression":
if (node.operator === "=") {
node.type = "AssignmentPattern";
delete node.operator;
} else {
this.toAssignable(prop, isBinding, "object destructuring pattern");
this.raise(node.left.end, "Only '=' operator can be used for specifying default value.");
}
break;
case "MemberExpression":
if (!isBinding) break;
default: {
const message = "Invalid left-hand side" +
(contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression");
this.raise(node.start, message);
}
}
}
return node;
}
// Convert list of expression atoms to binding list.
toAssignableList(
exprList: Expression[], isBinding: ?boolean, contextDescription: string): $ReadOnlyArray<Pattern> {
let end = exprList.length;
if (end) {
const last = exprList[end - 1];
if (last && last.type === "RestElement") {
--end;
} else if (last && last.type === "SpreadElement") {
last.type = "RestElement";
const arg = last.argument;
this.toAssignable(arg, isBinding, contextDescription);
if (
arg.type !== "Identifier" &&
arg.type !== "MemberExpression" &&
arg.type !== "ArrayPattern"
) {
this.unexpected(arg.start);
}
--end;
}
}
for (let i = 0; i < end; i++) {
const elt = exprList[i];
if (elt && elt.type === "SpreadElement")
this.raise(elt.start, "The rest element has to be the last element when destructuring");
if (elt) this.toAssignable(elt, isBinding, contextDescription);
}
return exprList;
}
// Convert list of expression atoms to a list of
toReferencedList(exprList: $ReadOnlyArray<?Expression>): $ReadOnlyArray<?Expression> {
return exprList;
}
// Parses spread element.
parseSpread<T : RestElement | SpreadElement>(refShorthandDefaultPos: ?Pos): T {
const node = this.startNode();
this.next();
node.argument = this.parseMaybeAssign(false, refShorthandDefaultPos);
return this.finishNode(node, "SpreadElement");
}
parseRest(): RestElement {
const node = this.startNode();
this.next();
node.argument = this.parseBindingAtom();
return this.finishNode(node, "RestElement");
}
shouldAllowYieldIdentifier(): boolean {
return this.match(tt._yield) && !this.state.strict && !this.state.inGenerator;
}
parseBindingIdentifier(): Identifier {
return this.parseIdentifier(this.shouldAllowYieldIdentifier());
}
// Parses lvalue (assignable) atom.
parseBindingAtom(): Pattern {
switch (this.state.type) {
case tt._yield:
case tt.name:
return this.parseBindingIdentifier();
case tt.bracketL:
const node = this.startNode();
this.next();
node.elements = this.parseBindingList(tt.bracketR, true);
return this.finishNode(node, "ArrayPattern");
case tt.braceL:
return this.parseObj(true);
default:
throw this.unexpected();
}
}
parseBindingList(close: TokenType, allowEmpty?: boolean): $ReadOnlyArray<Pattern> {
const elts = [];
let first = true;
while (!this.eat(close)) {
if (first) {
first = false;
} else {
this.expect(tt.comma);
}
if (allowEmpty && this.match(tt.comma)) {
// $FlowFixMe This method returns `$ReadOnlyArray<?Pattern>` if `allowEmpty` is set.
elts.push(null);
} else if (this.eat(close)) {
break;
} else if (this.match(tt.ellipsis)) {
elts.push(this.parseAssignableListItemTypes(this.parseRest()));
this.expect(close);
break;
} else {
const decorators = [];
while (this.match(tt.at)) {
decorators.push(this.parseDecorator());
}
const left = this.parseMaybeDefault();
if (decorators.length) {
left.decorators = decorators;
}
this.parseAssignableListItemTypes(left);
elts.push(this.parseMaybeDefault(left.start, left.loc.start, left));
}
}
return elts;
}
parseAssignableListItemTypes(param: Pattern): Pattern {
return param;
}
// Parses assignment pattern around given atom if possible.
parseMaybeDefault(startPos?: ?number, startLoc?: ?Position, left?: ?Pattern): Pattern {
startLoc = startLoc || this.state.startLoc;
startPos = startPos || this.state.start;
left = left || this.parseBindingAtom();
if (!this.eat(tt.eq)) return left;
const node = this.startNodeAt(startPos, startLoc);
node.left = left;
node.right = this.parseMaybeAssign();
return this.finishNode(node, "AssignmentPattern");
}
// Verify that a node is an lval — something that can be assigned
// to.
checkLVal(
expr: Expression,
isBinding: ?boolean,
checkClashes: ?{ [key: string]: boolean },
contextDescription: string): void {
switch (expr.type) {
case "PrivateName":
case "Identifier":
this.checkReservedWord(expr.name, expr.start, false, true);
if (checkClashes) {
// we need to prefix this with an underscore for the cases where we have a key of
// `__proto__`. there's a bug in old V8 where the following wouldn't work:
//
// > var obj = Object.create(null);
// undefined
// > obj.__proto__
// null
// > obj.__proto__ = true;
// true
// > obj.__proto__
// null
const key = `_${expr.name}`;
if (checkClashes[key]) {
this.raise(expr.start, "Argument name clash in strict mode");
} else {
checkClashes[key] = true;
}
}
break;
case "ObjectProperty":
this.toAssignable(node.value, isBinding, contextDescription);
case "MemberExpression":
if (isBinding)
this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " member expression");
break;
case "SpreadElement":
node.type = "RestElement";
break;
case "ArrayExpression":
node.type = "ArrayPattern";
this.toAssignableList(node.elements, isBinding, contextDescription);
break;
case "AssignmentExpression":
if (node.operator === "=") {
node.type = "AssignmentPattern";
delete node.operator;
} else {
this.raise(node.left.end, "Only '=' operator can be used for specifying default value.");
case "ObjectPattern":
for (let prop of expr.properties) {
if (prop.type === "ObjectProperty") prop = prop.value;
this.checkLVal(prop, isBinding, checkClashes, "object destructuring pattern");
}
break;
case "MemberExpression":
if (!isBinding) break;
case "ArrayPattern":
for (const elem of expr.elements) {
if (elem) this.checkLVal(elem, isBinding, checkClashes, "array destructuring pattern");
}
break;
case "AssignmentPattern":
this.checkLVal(expr.left, isBinding, checkClashes, "assignment pattern");
break;
case "RestElement":
this.checkLVal(expr.argument, isBinding, checkClashes, "rest element");
break;
default: {
const message = "Invalid left-hand side" +
const message = (isBinding ? /* istanbul ignore next */ "Binding invalid" : "Invalid") +
" left-hand side" +
(contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression");
this.raise(node.start, message);
this.raise(expr.start, message);
}
}
}
return node;
};
// Convert list of expression atoms to binding list.
pp.toAssignableList = function (exprList, isBinding, contextDescription) {
let end = exprList.length;
if (end) {
const last = exprList[end - 1];
if (last && last.type === "RestElement") {
--end;
} else if (last && last.type === "SpreadElement") {
last.type = "RestElement";
const arg = last.argument;
this.toAssignable(arg, isBinding, contextDescription);
if (arg.type !== "Identifier" && arg.type !== "MemberExpression" && arg.type !== "ArrayPattern") {
this.unexpected(arg.start);
}
--end;
}
}
for (let i = 0; i < end; i++) {
const elt = exprList[i];
if (elt && elt.type === "SpreadElement")
this.raise(elt.start, "The rest element has to be the last element when destructuring");
if (elt) this.toAssignable(elt, isBinding, contextDescription);
}
return exprList;
};
// Convert list of expression atoms to a list of
pp.toReferencedList = function (exprList) {
return exprList;
};
// Parses spread element.
pp.parseSpread = function (refShorthandDefaultPos) {
const node = this.startNode();
this.next();
node.argument = this.parseMaybeAssign(false, refShorthandDefaultPos);
return this.finishNode(node, "SpreadElement");
};
pp.parseRest = function () {
const node = this.startNode();
this.next();
node.argument = this.parseBindingAtom();
return this.finishNode(node, "RestElement");
};
pp.shouldAllowYieldIdentifier = function () {
return this.match(tt._yield) && !this.state.strict && !this.state.inGenerator;
};
pp.parseBindingIdentifier = function () {
return this.parseIdentifier(this.shouldAllowYieldIdentifier());
};
// Parses lvalue (assignable) atom.
pp.parseBindingAtom = function () {
switch (this.state.type) {
case tt._yield:
case tt.name:
return this.parseBindingIdentifier();
case tt.bracketL:
const node = this.startNode();
this.next();
node.elements = this.parseBindingList(tt.bracketR, true);
return this.finishNode(node, "ArrayPattern");
case tt.braceL:
return this.parseObj(true);
default:
this.unexpected();
}
};
pp.parseBindingList = function (close, allowEmpty) {
const elts = [];
let first = true;
while (!this.eat(close)) {
if (first) {
first = false;
} else {
this.expect(tt.comma);
}
if (allowEmpty && this.match(tt.comma)) {
elts.push(null);
} else if (this.eat(close)) {
break;
} else if (this.match(tt.ellipsis)) {
elts.push(this.parseAssignableListItemTypes(this.parseRest()));
this.expect(close);
break;
} else {
const decorators = [];
while (this.match(tt.at)) {
decorators.push(this.parseDecorator());
}
const left = this.parseMaybeDefault();
if (decorators.length) {
left.decorators = decorators;
}
this.parseAssignableListItemTypes(left);
elts.push(this.parseMaybeDefault(left.start, left.loc.start, left));
}
}
return elts;
};
pp.parseAssignableListItemTypes = function (param) {
return param;
};
// Parses assignment pattern around given atom if possible.
pp.parseMaybeDefault = function (startPos, startLoc, left) {
startLoc = startLoc || this.state.startLoc;
startPos = startPos || this.state.start;
left = left || this.parseBindingAtom();
if (!this.eat(tt.eq)) return left;
const node = this.startNodeAt(startPos, startLoc);
node.left = left;
node.right = this.parseMaybeAssign();
return this.finishNode(node, "AssignmentPattern");
};
// Verify that a node is an lval — something that can be assigned
// to.
pp.checkLVal = function (expr, isBinding, checkClashes, contextDescription) {
switch (expr.type) {
case "Identifier":
this.checkReservedWord(expr.name, expr.start, false, true);
if (checkClashes) {
// we need to prefix this with an underscore for the cases where we have a key of
// `__proto__`. there's a bug in old V8 where the following wouldn't work:
//
// > var obj = Object.create(null);
// undefined
// > obj.__proto__
// null
// > obj.__proto__ = true;
// true
// > obj.__proto__
// null
const key = `_${expr.name}`;
if (checkClashes[key]) {
this.raise(expr.start, "Argument name clash in strict mode");
} else {
checkClashes[key] = true;
}
}
break;
case "MemberExpression":
if (isBinding) this.raise(expr.start, (isBinding ? "Binding" : "Assigning to") + " member expression");
break;
case "ObjectPattern":
for (let prop of (expr.properties: Array<Object>)) {
if (prop.type === "ObjectProperty") prop = prop.value;
this.checkLVal(prop, isBinding, checkClashes, "object destructuring pattern");
}
break;
case "ArrayPattern":
for (const elem of (expr.elements: Array<Object>)) {
if (elem) this.checkLVal(elem, isBinding, checkClashes, "array destructuring pattern");
}
break;
case "AssignmentPattern":
this.checkLVal(expr.left, isBinding, checkClashes, "assignment pattern");
break;
case "RestElement":
this.checkLVal(expr.argument, isBinding, checkClashes, "rest element");
break;
default: {
const message = (isBinding ? /* istanbul ignore next */ "Binding invalid" : "Invalid") +
" left-hand side" +
(contextDescription ? " in " + contextDescription : /* istanbul ignore next */ "expression");
this.raise(expr.start, message);
}
}
};
}

View File

@@ -1,13 +1,16 @@
// @flow
import Parser from "./index";
import UtilParser from "./util";
import { SourceLocation, type Position } from "../util/location";
import type { Comment, Node as NodeType, NodeBase } from "../types";
// Start an AST node, attaching a start offset.
const pp = Parser.prototype;
const commentKeys = ["leadingComments", "trailingComments", "innerComments"];
class Node {
constructor(parser?: Parser, pos?: number, loc?: Position) {
class Node implements NodeBase {
constructor(parser: Parser, pos: number, loc: Position) {
this.type = "";
this.start = pos;
this.end = 0;
@@ -17,15 +20,22 @@ class Node {
}
type: string;
start: ?number;
start: number;
end: number;
loc: SourceLocation;
range: [number, number];
leadingComments: ?Array<Comment>;
trailingComments: ?Array<Comment>;
innerComments: ?Array<Comment>;
extra: { [key: string]: any };
__clone(): Node {
const node2 = new Node;
__clone(): this {
// $FlowIgnore
const node2: any = new Node;
for (const key in this) {
// Do not clone comments that are already attached to the node
if (commentKeys.indexOf(key) < 0) {
// $FlowIgnore
node2[key] = this[key];
}
}
@@ -34,43 +44,40 @@ class Node {
}
}
pp.startNode = function () {
return new Node(this, this.state.start, this.state.startLoc);
};
export class NodeUtils extends UtilParser {
startNode<T : NodeType>(): T {
// $FlowIgnore
return new Node(this, this.state.start, this.state.startLoc);
}
pp.startNodeAt = function (pos, loc) {
return new Node(this, pos, loc);
};
startNodeAt<T : NodeType>(pos: number, loc: Position): T {
// $FlowIgnore
return new Node(this, pos, loc);
}
function finishNodeAt(node, type, pos, loc) {
node.type = type;
node.end = pos;
node.loc.end = loc;
if (this.options.ranges) node.range[1] = pos;
this.processComment(node);
return node;
// Finish an AST node, adding `type` and `end` properties.
finishNode<T : NodeType>(node: T, type: string): T {
return this.finishNodeAt(node, type, this.state.lastTokEnd, this.state.lastTokEndLoc);
}
// Finish node at given position
finishNodeAt<T : NodeType>(node: T, type: string, pos: number, loc: Position): T {
node.type = type;
node.end = pos;
node.loc.end = loc;
if (this.options.ranges) node.range[1] = pos;
this.processComment(node);
return node;
}
/**
* Reset the start location of node to the start location of locationNode
*/
resetStartLocationFromNode(node: NodeBase, locationNode: NodeBase): void {
node.start = locationNode.start;
node.loc.start = locationNode.loc.start;
if (this.options.ranges) node.range[0] = locationNode.range[0];
}
}
// Finish an AST node, adding `type` and `end` properties.
pp.finishNode = function (node, type) {
return finishNodeAt.call(this, node, type, this.state.lastTokEnd, this.state.lastTokEndLoc);
};
// Finish node at given position
pp.finishNodeAt = function (node, type, pos, loc) {
return finishNodeAt.call(this, node, type, pos, loc);
};
/**
* Reset the start location of node to the start location of locationNode
*/
pp.resetStartLocationFromNode = function (node, locationNode) {
node.start = locationNode.start;
node.loc.start = locationNode.loc.start;
if (this.options.ranges) node.range[0] = locationNode.range[0];
return node;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +1,91 @@
import { types as tt } from "../tokenizer/types";
import Parser from "./index";
import { lineBreak } from "../util/whitespace";
// @flow
const pp = Parser.prototype;
import { types as tt, type TokenType } from "../tokenizer/types";
import Tokenizer from "../tokenizer";
import type { Node } from "../types";
import { lineBreak } from "../util/whitespace";
// ## Parser utilities
// TODO
export default class UtilParser extends Tokenizer {
// TODO
pp.addExtra = function (node, key, val) {
if (!node) return;
addExtra(node: Node, key: string, val: any): void {
if (!node) return;
const extra = node.extra = node.extra || {};
extra[key] = val;
};
// TODO
pp.isRelational = function (op) {
return this.match(tt.relational) && this.state.value === op;
};
// TODO
pp.expectRelational = function (op) {
if (this.isRelational(op)) {
this.next();
} else {
this.unexpected(null, tt.relational);
const extra = node.extra = node.extra || {};
extra[key] = val;
}
};
// Tests whether parsed token is a contextual keyword.
// TODO
pp.isContextual = function (name) {
return this.match(tt.name) && this.state.value === name;
};
// Consumes contextual keyword if possible.
pp.eatContextual = function (name) {
return this.state.value === name && this.eat(tt.name);
};
// Asserts that following token is given contextual keyword.
pp.expectContextual = function (name, message) {
if (!this.eatContextual(name)) this.unexpected(null, message);
};
// Test whether a semicolon can be inserted at the current position.
pp.canInsertSemicolon = function () {
return this.match(tt.eof) ||
this.match(tt.braceR) ||
lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start));
};
// TODO
pp.isLineTerminator = function () {
return this.eat(tt.semi) || this.canInsertSemicolon();
};
// Consume a semicolon, or, failing that, see if we are allowed to
// pretend that there is a semicolon at this position.
pp.semicolon = function () {
if (!this.isLineTerminator()) this.unexpected(null, tt.semi);
};
// Expect a token of a given type. If found, consume it, otherwise,
// raise an unexpected token error at given pos.
pp.expect = function (type, pos) {
return this.eat(type) || this.unexpected(pos, type);
};
// Raise an unexpected token error. Can take the expected token type
// instead of a message string.
pp.unexpected = function (pos, messageOrType = "Unexpected token") {
if (messageOrType && typeof messageOrType === "object" && messageOrType.label) {
messageOrType = `Unexpected token, expected ${messageOrType.label}`;
isRelational(op: "<" | ">"): boolean {
return this.match(tt.relational) && this.state.value === op;
}
this.raise(pos != null ? pos : this.state.start, messageOrType);
};
// TODO
expectRelational(op: "<" | ">"): void {
if (this.isRelational(op)) {
this.next();
} else {
this.unexpected(null, tt.relational);
}
}
// Tests whether parsed token is a contextual keyword.
isContextual(name: string): boolean {
return this.match(tt.name) && this.state.value === name;
}
// Consumes contextual keyword if possible.
eatContextual(name: string): boolean {
return this.state.value === name && this.eat(tt.name);
}
// Asserts that following token is given contextual keyword.
expectContextual(name: string, message?: string): void {
if (!this.eatContextual(name)) this.unexpected(null, message);
}
// Test whether a semicolon can be inserted at the current position.
canInsertSemicolon(): boolean {
return this.match(tt.eof) ||
this.match(tt.braceR) ||
lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start));
}
// TODO
isLineTerminator(): boolean {
return this.eat(tt.semi) || this.canInsertSemicolon();
}
// Consume a semicolon, or, failing that, see if we are allowed to
// pretend that there is a semicolon at this position.
semicolon(): void {
if (!this.isLineTerminator()) this.unexpected(null, tt.semi);
}
// Expect a token of a given type. If found, consume it, otherwise,
// raise an unexpected token error at given pos.
expect(type: TokenType, pos?: ?number): void {
this.eat(type) || this.unexpected(pos, type);
}
// Raise an unexpected token error. Can take the expected token type
// instead of a message string.
unexpected(pos: ?number, messageOrType: string | TokenType = "Unexpected token"): empty {
if (typeof messageOrType !== "string") {
messageOrType = `Unexpected token, expected ${messageOrType.label}`;
}
throw this.raise(pos != null ? pos : this.state.start, messageOrType);
}
}

View File

@@ -1,248 +1,234 @@
// @flow
import { types as tt } from "../tokenizer/types";
import Parser from "../parser";
import type Parser from "../parser";
import * as N from "../types";
const pp = Parser.prototype;
pp.estreeParseRegExpLiteral = function ({ pattern, flags }) {
let regex = null;
try {
regex = new RegExp(pattern, flags);
} catch (e) {
// In environments that don't support these flags value will
// be null as the regex can't be represented natively.
}
const node = this.estreeParseLiteral(regex);
node.regex = { pattern, flags };
return node;
};
pp.estreeParseLiteral = function (value) {
return this.parseLiteral(value, "Literal");
};
pp.directiveToStmt = function (directive) {
const directiveLiteral = directive.value;
const stmt = this.startNodeAt(directive.start, directive.loc.start);
const expression = this.startNodeAt(directiveLiteral.start, directiveLiteral.loc.start);
expression.value = directiveLiteral.value;
expression.raw = directiveLiteral.extra.raw;
stmt.expression = this.finishNodeAt(expression, "Literal", directiveLiteral.end, directiveLiteral.loc.end);
stmt.directive = directiveLiteral.extra.raw.slice(1, -1);
return this.finishNodeAt(stmt, "ExpressionStatement", directive.end, directive.loc.end);
};
function isSimpleProperty(node) {
return node &&
function isSimpleProperty(node: N.Node): boolean {
return node != null &&
node.type === "Property" &&
node.kind === "init" &&
node.method === false;
}
export default function (instance) {
instance.extend("checkDeclaration", function(inner) {
return function (node) {
if (isSimpleProperty(node)) {
this.checkDeclaration(node.value);
export default (superClass: Class<Parser>): Class<Parser> => class extends superClass {
estreeParseRegExpLiteral({ pattern, flags }: N.RegExpLiteral): N.Node {
let regex = null;
try {
regex = new RegExp(pattern, flags);
} catch (e) {
// In environments that don't support these flags value will
// be null as the regex can't be represented natively.
}
const node = this.estreeParseLiteral(regex);
node.regex = { pattern, flags };
return node;
}
estreeParseLiteral(value: any): N.Node {
return this.parseLiteral(value, "Literal");
}
directiveToStmt(directive: N.Directive): N.ExpressionStatement {
const directiveLiteral = directive.value;
const stmt = this.startNodeAt(directive.start, directive.loc.start);
const expression = this.startNodeAt(directiveLiteral.start, directiveLiteral.loc.start);
expression.value = directiveLiteral.value;
expression.raw = directiveLiteral.extra.raw;
stmt.expression = this.finishNodeAt(
expression, "Literal", directiveLiteral.end, directiveLiteral.loc.end);
stmt.directive = directiveLiteral.extra.raw.slice(1, -1);
return this.finishNodeAt(stmt, "ExpressionStatement", directive.end, directive.loc.end);
}
// ==================================
// Overrides
// ==================================
checkDeclaration(node: N.Pattern): void {
if (isSimpleProperty(node)) {
// $FlowFixMe
this.checkDeclaration(node.value);
} else {
super.checkDeclaration(node);
}
}
checkGetterSetterParamCount(prop: N.ObjectMethod | N.ClassMethod): void {
const paramCount = prop.kind === "get" ? 0 : 1;
// $FlowFixMe (prop.value present for ObjectMethod, but for ClassMethod should use prop.params?)
if (prop.value.params.length !== paramCount) {
const start = prop.start;
if (prop.kind === "get") {
this.raise(start, "getter should have no params");
} else {
inner.call(this, node);
this.raise(start, "setter should have exactly one param");
}
};
});
}
}
instance.extend("checkGetterSetterParamCount", function() {
return function (prop) {
const paramCount = prop.kind === "get" ? 0 : 1;
if (prop.value.params.length !== paramCount) {
const start = prop.start;
if (prop.kind === "get") {
this.raise(start, "getter should have no params");
checkLVal(
expr: N.Expression, isBinding: ?boolean, checkClashes: ?{ [key: string]: boolean }, ...args): void {
switch (expr.type) {
case "ObjectPattern":
expr.properties.forEach((prop) => {
this.checkLVal(
prop.type === "Property" ? prop.value : prop,
isBinding,
checkClashes,
"object destructuring pattern"
);
});
break;
default:
super.checkLVal(expr, isBinding, checkClashes, ...args);
}
}
checkPropClash(prop: N.ObjectMember, propHash: { [key: string]: boolean }): void {
if (prop.computed || !isSimpleProperty(prop)) return;
const key = prop.key;
// It is either an Identifier or a String/NumericLiteral
const name = key.type === "Identifier" ? key.name : String(key.value);
if (name === "__proto__") {
if (propHash.proto) this.raise(key.start, "Redefinition of __proto__ property");
propHash.proto = true;
}
}
isStrictBody(node: { body: N.BlockStatement }, isExpression?: boolean): boolean {
if (!isExpression && node.body.body.length > 0) {
for (const directive of node.body.body) {
if (directive.type === "ExpressionStatement" && directive.expression.type === "Literal") {
if (directive.expression.value === "use strict") return true;
} else {
this.raise(start, "setter should have exactly one param");
}
}
};
});
instance.extend("checkLVal", function(inner) {
return function (expr, isBinding, checkClashes, ...args) {
switch (expr.type) {
case "ObjectPattern":
expr.properties.forEach((prop) => {
this.checkLVal(
prop.type === "Property" ? prop.value : prop,
isBinding,
checkClashes,
"object destructuring pattern"
);
});
// Break for the first non literal expression
break;
default:
inner.call(this, expr, isBinding, checkClashes, ...args);
}
}
};
});
}
instance.extend("checkPropClash", function () {
return function (prop, propHash) {
if (prop.computed || !isSimpleProperty(prop)) return;
return false;
}
const key = prop.key;
// It is either an Identifier or a String/NumericLiteral
const name = key.type === "Identifier" ? key.name : String(key.value);
isValidDirective(stmt: N.Statement): boolean {
return stmt.type === "ExpressionStatement" &&
stmt.expression.type === "Literal" &&
typeof stmt.expression.value === "string" &&
(!stmt.expression.extra || !stmt.expression.extra.parenthesized);
}
if (name === "__proto__") {
if (propHash.proto) this.raise(key.start, "Redefinition of __proto__ property");
propHash.proto = true;
}
};
});
parseBlockBody(node: N.BlockStatementLike, ...args): void {
super.parseBlockBody(node, ...args);
instance.extend("isStrictBody", function () {
return function (node, isExpression) {
if (!isExpression && node.body.body.length > 0) {
for (const directive of (node.body.body: Array<Object>)) {
if (directive.type === "ExpressionStatement" && directive.expression.type === "Literal") {
if (directive.expression.value === "use strict") return true;
} else {
// Break for the first non literal expression
break;
}
const directiveStatements = node.directives.map((d) => this.directiveToStmt(d));
node.body = directiveStatements.concat(node.body);
delete node.directives;
}
parseClassMethod(classBody: N.ClassBody, ...args) {
super.parseClassMethod(classBody, ...args);
const body = classBody.body;
// $FlowIgnore
body[body.length - 1].type = "MethodDefinition";
}
parseExprAtom(...args): N.Expression {
switch (this.state.type) {
case tt.regexp:
return this.estreeParseRegExpLiteral(this.state.value);
case tt.num:
case tt.string:
return this.estreeParseLiteral(this.state.value);
case tt._null:
return this.estreeParseLiteral(null);
case tt._true:
return this.estreeParseLiteral(true);
case tt._false:
return this.estreeParseLiteral(false);
default:
return super.parseExprAtom(...args);
}
}
parseLiteral<T : N.Literal>(...args): T {
const node = super.parseLiteral(...args);
node.raw = node.extra.raw;
delete node.extra;
return node;
}
parseMethod(node: N.MethodLike, ...args): N.MethodLike {
let funcNode = this.startNode();
funcNode.kind = node.kind; // provide kind, so super method correctly sets state
funcNode = super.parseMethod(funcNode, ...args);
delete funcNode.kind;
// $FlowIgnore
node.value = this.finishNode(funcNode, "FunctionExpression");
return node;
}
parseObjectMethod(...args): ?N.ObjectMethod {
const node = super.parseObjectMethod(...args);
if (node) {
// $FlowIgnore
if (node.kind === "method") node.kind = "init";
// $FlowIgnore
node.type = "Property";
}
return node;
}
parseObjectProperty(...args): ?N.ObjectProperty {
const node = super.parseObjectProperty(...args);
if (node) {
// $FlowIgnore
node.kind = "init";
// $FlowIgnore
node.type = "Property";
}
return node;
}
toAssignable(node: N.Node, isBinding: ?boolean, ...args): N.Node {
if (isSimpleProperty(node)) {
this.toAssignable(node.value, isBinding, ...args);
return node;
} else if (node.type === "ObjectExpression") {
node.type = "ObjectPattern";
for (const prop of node.properties) {
if (prop.kind === "get" || prop.kind === "set") {
this.raise(prop.key.start, "Object pattern can't contain getter or setter");
} else if (prop.method) {
this.raise(prop.key.start, "Object pattern can't contain methods");
} else {
this.toAssignable(prop, isBinding, "object destructuring pattern");
}
}
return false;
};
});
instance.extend("isValidDirective", function () {
return function (stmt) {
return stmt.type === "ExpressionStatement" &&
stmt.expression.type === "Literal" &&
typeof stmt.expression.value === "string" &&
(!stmt.expression.extra || !stmt.expression.extra.parenthesized);
};
});
instance.extend("parseBlockBody", function (inner) {
return function (node, ...args) {
inner.call(this, node, ...args);
node.directives.reverse().forEach((directive) => {
node.body.unshift(this.directiveToStmt(directive));
});
delete node.directives;
};
});
instance.extend("parseClassMethod", function (inner) {
return function (classBody, ...args) {
inner.call(this, classBody, ...args);
const body = classBody.body;
body[body.length - 1].type = "MethodDefinition";
};
});
instance.extend("parseExprAtom", function(inner) {
return function (...args) {
switch (this.state.type) {
case tt.regexp:
return this.estreeParseRegExpLiteral(this.state.value);
case tt.num:
case tt.string:
return this.estreeParseLiteral(this.state.value);
case tt._null:
return this.estreeParseLiteral(null);
case tt._true:
return this.estreeParseLiteral(true);
case tt._false:
return this.estreeParseLiteral(false);
default:
return inner.call(this, ...args);
}
};
});
instance.extend("parseLiteral", function(inner) {
return function (...args) {
const node = inner.call(this, ...args);
node.raw = node.extra.raw;
delete node.extra;
return node;
};
});
}
instance.extend("parseMethod", function(inner) {
return function (node, ...args) {
let funcNode = this.startNode();
funcNode.kind = node.kind; // provide kind, so inner method correctly sets state
funcNode = inner.call(this, funcNode, ...args);
delete funcNode.kind;
node.value = this.finishNode(funcNode, "FunctionExpression");
return node;
};
});
instance.extend("parseObjectMethod", function(inner) {
return function (...args) {
const node = inner.call(this, ...args);
if (node) {
if (node.kind === "method") node.kind = "init";
node.type = "Property";
}
return node;
};
});
instance.extend("parseObjectProperty", function(inner) {
return function (...args) {
const node = inner.call(this, ...args);
if (node) {
node.kind = "init";
node.type = "Property";
}
return node;
};
});
instance.extend("toAssignable", function(inner) {
return function (node, isBinding, ...args) {
if (isSimpleProperty(node)) {
this.toAssignable(node.value, isBinding, ...args);
return node;
} else if (node.type === "ObjectExpression") {
node.type = "ObjectPattern";
for (const prop of (node.properties: Array<Object>)) {
if (prop.kind === "get" || prop.kind === "set") {
this.raise(prop.key.start, "Object pattern can't contain getter or setter");
} else if (prop.method) {
this.raise(prop.key.start, "Object pattern can't contain methods");
} else {
this.toAssignable(prop, isBinding, "object destructuring pattern");
}
}
return node;
}
return inner.call(this, node, isBinding, ...args);
};
});
}
return super.toAssignable(node, isBinding, ...args);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,12 @@
// @flow
import XHTMLEntities from "./xhtml";
import type Parser from "../../parser";
import { TokenType, types as tt } from "../../tokenizer/types";
import { TokContext, types as tc } from "../../tokenizer/context";
import Parser from "../../parser";
import * as N from "../../types";
import { isIdentifierChar, isIdentifierStart } from "../../util/identifier";
import type { Pos, Position } from "../../util/location";
import { isNewLine } from "../../util/whitespace";
const HEX_NUMBER = /^[\da-fA-F]+$/;
@@ -33,147 +37,9 @@ tt.jsxTagEnd.updateContext = function(prevType) {
}
};
const pp = Parser.prototype;
// Reads inline JSX contents token.
pp.jsxReadToken = function() {
let out = "";
let chunkStart = this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated JSX contents");
}
const ch = this.input.charCodeAt(this.state.pos);
switch (ch) {
case 60: // "<"
case 123: // "{"
if (this.state.pos === this.state.start) {
if (ch === 60 && this.state.exprAllowed) {
++this.state.pos;
return this.finishToken(tt.jsxTagStart);
}
return this.getTokenFromCode(ch);
}
out += this.input.slice(chunkStart, this.state.pos);
return this.finishToken(tt.jsxText, out);
case 38: // "&"
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
break;
default:
if (isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(true);
chunkStart = this.state.pos;
} else {
++this.state.pos;
}
}
}
};
pp.jsxReadNewLine = function(normalizeCRLF) {
const ch = this.input.charCodeAt(this.state.pos);
let out;
++this.state.pos;
if (ch === 13 && this.input.charCodeAt(this.state.pos) === 10) {
++this.state.pos;
out = normalizeCRLF ? "\n" : "\r\n";
} else {
out = String.fromCharCode(ch);
}
++this.state.curLine;
this.state.lineStart = this.state.pos;
return out;
};
pp.jsxReadString = function(quote) {
let out = "";
let chunkStart = ++this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated string constant");
}
const ch = this.input.charCodeAt(this.state.pos);
if (ch === quote) break;
if (ch === 38) { // "&"
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
} else if (isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(false);
chunkStart = this.state.pos;
} else {
++this.state.pos;
}
}
out += this.input.slice(chunkStart, this.state.pos++);
return this.finishToken(tt.string, out);
};
pp.jsxReadEntity = function() {
let str = "";
let count = 0;
let entity;
let ch = this.input[this.state.pos];
const startPos = ++this.state.pos;
while (this.state.pos < this.input.length && count++ < 10) {
ch = this.input[this.state.pos++];
if (ch === ";") {
if (str[0] === "#") {
if (str[1] === "x") {
str = str.substr(2);
if (HEX_NUMBER.test(str))
entity = String.fromCodePoint(parseInt(str, 16));
} else {
str = str.substr(1);
if (DECIMAL_NUMBER.test(str))
entity = String.fromCodePoint(parseInt(str, 10));
}
} else {
entity = XHTMLEntities[str];
}
break;
}
str += ch;
}
if (!entity) {
this.state.pos = startPos;
return "&";
}
return entity;
};
// Read a JSX identifier (valid tag or attribute name).
//
// Optimized version since JSX identifiers can"t contain
// escape characters and so can be read as single slice.
// Also assumes that first character was already checked
// by isIdentifierStart in readToken.
pp.jsxReadWord = function() {
let ch;
const start = this.state.pos;
do {
ch = this.input.charCodeAt(++this.state.pos);
} while (isIdentifierChar(ch) || ch === 45); // "-"
return this.finishToken(tt.jsxName, this.input.slice(start, this.state.pos));
};
// Transforms JSX element name to string.
function getQualifiedJSXName(object) {
function getQualifiedJSXName(object: N.JSXIdentifier | N.JSXNamespacedName | N.JSXMemberExpression): string {
if (object.type === "JSXIdentifier") {
return object.name;
}
@@ -185,282 +51,421 @@ function getQualifiedJSXName(object) {
if (object.type === "JSXMemberExpression") {
return getQualifiedJSXName(object.object) + "." + getQualifiedJSXName(object.property);
}
// istanbul ignore next
throw new Error("Node had unexpected type: " + object.type);
}
// Parse next token as JSX identifier
export default (superClass: Class<Parser>): Class<Parser> => class extends superClass {
// Reads inline JSX contents token.
pp.jsxParseIdentifier = function() {
const node = this.startNode();
if (this.match(tt.jsxName)) {
node.name = this.state.value;
} else if (this.state.type.keyword) {
node.name = this.state.type.keyword;
} else {
this.unexpected();
}
this.next();
return this.finishNode(node, "JSXIdentifier");
};
// Parse namespaced identifier.
pp.jsxParseNamespacedName = function() {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
const name = this.jsxParseIdentifier();
if (!this.eat(tt.colon)) return name;
const node = this.startNodeAt(startPos, startLoc);
node.namespace = name;
node.name = this.jsxParseIdentifier();
return this.finishNode(node, "JSXNamespacedName");
};
// Parses element name in any form - namespaced, member
// or single identifier.
pp.jsxParseElementName = function() {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
let node = this.jsxParseNamespacedName();
while (this.eat(tt.dot)) {
const newNode = this.startNodeAt(startPos, startLoc);
newNode.object = node;
newNode.property = this.jsxParseIdentifier();
node = this.finishNode(newNode, "JSXMemberExpression");
}
return node;
};
// Parses any type of JSX attribute value.
pp.jsxParseAttributeValue = function() {
let node;
switch (this.state.type) {
case tt.braceL:
node = this.jsxParseExpressionContainer();
if (node.expression.type === "JSXEmptyExpression") {
this.raise(node.start, "JSX attributes must only be assigned a non-empty expression");
} else {
return node;
jsxReadToken(): void {
let out = "";
let chunkStart = this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated JSX contents");
}
case tt.jsxTagStart:
case tt.string:
return this.parseExprAtom();
const ch = this.input.charCodeAt(this.state.pos);
default:
this.raise(this.state.start, "JSX value should be either an expression or a quoted JSX text");
}
};
// JSXEmptyExpression is unique type since it doesn't actually parse anything,
// and so it should start at the end of last read token (left brace) and finish
// at the beginning of the next one (right brace).
pp.jsxParseEmptyExpression = function() {
const node = this.startNodeAt(this.state.lastTokEnd, this.state.lastTokEndLoc);
return this.finishNodeAt(node, "JSXEmptyExpression", this.state.start, this.state.startLoc);
};
// Parse JSX spread child
pp.jsxParseSpreadChild = function() {
const node = this.startNode();
this.expect(tt.braceL);
this.expect(tt.ellipsis);
node.expression = this.parseExpression();
this.expect(tt.braceR);
return this.finishNode(node, "JSXSpreadChild");
};
// Parses JSX expression enclosed into curly brackets.
pp.jsxParseExpressionContainer = function() {
const node = this.startNode();
this.next();
if (this.match(tt.braceR)) {
node.expression = this.jsxParseEmptyExpression();
} else {
node.expression = this.parseExpression();
}
this.expect(tt.braceR);
return this.finishNode(node, "JSXExpressionContainer");
};
// Parses following JSX attribute name-value pair.
pp.jsxParseAttribute = function() {
const node = this.startNode();
if (this.eat(tt.braceL)) {
this.expect(tt.ellipsis);
node.argument = this.parseMaybeAssign();
this.expect(tt.braceR);
return this.finishNode(node, "JSXSpreadAttribute");
}
node.name = this.jsxParseNamespacedName();
node.value = this.eat(tt.eq) ? this.jsxParseAttributeValue() : null;
return this.finishNode(node, "JSXAttribute");
};
// Parses JSX opening tag starting after "<".
pp.jsxParseOpeningElementAt = function(startPos, startLoc) {
const node = this.startNodeAt(startPos, startLoc);
node.attributes = [];
node.name = this.jsxParseElementName();
while (!this.match(tt.slash) && !this.match(tt.jsxTagEnd)) {
node.attributes.push(this.jsxParseAttribute());
}
node.selfClosing = this.eat(tt.slash);
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXOpeningElement");
};
// Parses JSX closing tag starting after "</".
pp.jsxParseClosingElementAt = function(startPos, startLoc) {
const node = this.startNodeAt(startPos, startLoc);
node.name = this.jsxParseElementName();
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXClosingElement");
};
// Parses entire JSX element, including it"s opening tag
// (starting after "<"), attributes, contents and closing tag.
pp.jsxParseElementAt = function(startPos, startLoc) {
const node = this.startNodeAt(startPos, startLoc);
const children = [];
const openingElement = this.jsxParseOpeningElementAt(startPos, startLoc);
let closingElement = null;
if (!openingElement.selfClosing) {
contents: for (;;) {
switch (this.state.type) {
case tt.jsxTagStart:
startPos = this.state.start; startLoc = this.state.startLoc;
this.next();
if (this.eat(tt.slash)) {
closingElement = this.jsxParseClosingElementAt(startPos, startLoc);
break contents;
switch (ch) {
case 60: // "<"
case 123: // "{"
if (this.state.pos === this.state.start) {
if (ch === 60 && this.state.exprAllowed) {
++this.state.pos;
return this.finishToken(tt.jsxTagStart);
}
return this.getTokenFromCode(ch);
}
children.push(this.jsxParseElementAt(startPos, startLoc));
out += this.input.slice(chunkStart, this.state.pos);
return this.finishToken(tt.jsxText, out);
case 38: // "&"
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
break;
case tt.jsxText:
children.push(this.parseExprAtom());
break;
case tt.braceL:
if (this.lookahead().type === tt.ellipsis) {
children.push(this.jsxParseSpreadChild());
} else {
children.push(this.jsxParseExpressionContainer());
}
break;
// istanbul ignore next - should never happen
default:
this.unexpected();
if (isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(true);
chunkStart = this.state.pos;
} else {
++this.state.pos;
}
}
}
}
if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
this.raise(
closingElement.start,
"Expected corresponding JSX closing tag for <" + getQualifiedJSXName(openingElement.name) + ">"
);
jsxReadNewLine(normalizeCRLF: boolean): string {
const ch = this.input.charCodeAt(this.state.pos);
let out;
++this.state.pos;
if (ch === 13 && this.input.charCodeAt(this.state.pos) === 10) {
++this.state.pos;
out = normalizeCRLF ? "\n" : "\r\n";
} else {
out = String.fromCharCode(ch);
}
++this.state.curLine;
this.state.lineStart = this.state.pos;
return out;
}
node.openingElement = openingElement;
node.closingElement = closingElement;
node.children = children;
if (this.match(tt.relational) && this.state.value === "<") {
this.raise(this.state.start, "Adjacent JSX elements must be wrapped in an enclosing tag");
}
return this.finishNode(node, "JSXElement");
};
jsxReadString(quote: number): void {
let out = "";
let chunkStart = ++this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated string constant");
}
// Parses entire JSX element from current position.
pp.jsxParseElement = function() {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
this.next();
return this.jsxParseElementAt(startPos, startLoc);
};
export default function(instance) {
instance.extend("parseExprAtom", function(inner) {
return function(refShortHandDefaultPos) {
if (this.match(tt.jsxText)) {
return this.parseLiteral(this.state.value, "JSXText");
} else if (this.match(tt.jsxTagStart)) {
return this.jsxParseElement();
const ch = this.input.charCodeAt(this.state.pos);
if (ch === quote) break;
if (ch === 38) { // "&"
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
} else if (isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(false);
chunkStart = this.state.pos;
} else {
return inner.call(this, refShortHandDefaultPos);
}
};
});
instance.extend("readToken", function(inner) {
return function(code) {
if (this.state.inPropertyName) return inner.call(this, code);
const context = this.curContext();
if (context === tc.j_expr) {
return this.jsxReadToken();
}
if (context === tc.j_oTag || context === tc.j_cTag) {
if (isIdentifierStart(code)) {
return this.jsxReadWord();
}
if (code === 62) {
++this.state.pos;
return this.finishToken(tt.jsxTagEnd);
}
if ((code === 34 || code === 39) && context === tc.j_oTag) {
return this.jsxReadString(code);
}
}
if (code === 60 && this.state.exprAllowed) {
++this.state.pos;
return this.finishToken(tt.jsxTagStart);
}
}
out += this.input.slice(chunkStart, this.state.pos++);
return this.finishToken(tt.string, out);
}
return inner.call(this, code);
};
});
jsxReadEntity(): string {
let str = "";
let count = 0;
let entity;
let ch = this.input[this.state.pos];
instance.extend("updateContext", function(inner) {
return function(prevType) {
if (this.match(tt.braceL)) {
const curContext = this.curContext();
if (curContext === tc.j_oTag) {
this.state.context.push(tc.braceExpression);
} else if (curContext === tc.j_expr) {
this.state.context.push(tc.templateQuasi);
const startPos = ++this.state.pos;
while (this.state.pos < this.input.length && count++ < 10) {
ch = this.input[this.state.pos++];
if (ch === ";") {
if (str[0] === "#") {
if (str[1] === "x") {
str = str.substr(2);
if (HEX_NUMBER.test(str))
entity = String.fromCodePoint(parseInt(str, 16));
} else {
str = str.substr(1);
if (DECIMAL_NUMBER.test(str))
entity = String.fromCodePoint(parseInt(str, 10));
}
} else {
inner.call(this, prevType);
entity = XHTMLEntities[str];
}
this.state.exprAllowed = true;
} else if (this.match(tt.slash) && prevType === tt.jsxTagStart) {
this.state.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
this.state.context.push(tc.j_cTag); // reconsider as closing tag context
this.state.exprAllowed = false;
} else {
return inner.call(this, prevType);
break;
}
};
});
}
str += ch;
}
if (!entity) {
this.state.pos = startPos;
return "&";
}
return entity;
}
// Read a JSX identifier (valid tag or attribute name).
//
// Optimized version since JSX identifiers can"t contain
// escape characters and so can be read as single slice.
// Also assumes that first character was already checked
// by isIdentifierStart in readToken.
jsxReadWord(): void {
let ch;
const start = this.state.pos;
do {
ch = this.input.charCodeAt(++this.state.pos);
} while (isIdentifierChar(ch) || ch === 45); // "-"
return this.finishToken(tt.jsxName, this.input.slice(start, this.state.pos));
}
// Parse next token as JSX identifier
jsxParseIdentifier(): N.JSXIdentifier {
const node = this.startNode();
if (this.match(tt.jsxName)) {
node.name = this.state.value;
} else if (this.state.type.keyword) {
node.name = this.state.type.keyword;
} else {
this.unexpected();
}
this.next();
return this.finishNode(node, "JSXIdentifier");
}
// Parse namespaced identifier.
jsxParseNamespacedName(): N.JSXNamespacedName {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
const name = this.jsxParseIdentifier();
if (!this.eat(tt.colon)) return name;
const node = this.startNodeAt(startPos, startLoc);
node.namespace = name;
node.name = this.jsxParseIdentifier();
return this.finishNode(node, "JSXNamespacedName");
}
// Parses element name in any form - namespaced, member
// or single identifier.
jsxParseElementName(): N.JSXNamespacedName | N.JSXMemberExpression {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
let node = this.jsxParseNamespacedName();
while (this.eat(tt.dot)) {
const newNode = this.startNodeAt(startPos, startLoc);
newNode.object = node;
newNode.property = this.jsxParseIdentifier();
node = this.finishNode(newNode, "JSXMemberExpression");
}
return node;
}
// Parses any type of JSX attribute value.
jsxParseAttributeValue(): N.Expression {
let node;
switch (this.state.type) {
case tt.braceL:
node = this.jsxParseExpressionContainer();
if (node.expression.type === "JSXEmptyExpression") {
throw this.raise(node.start, "JSX attributes must only be assigned a non-empty expression");
} else {
return node;
}
case tt.jsxTagStart:
case tt.string:
return this.parseExprAtom();
default:
throw this.raise(this.state.start, "JSX value should be either an expression or a quoted JSX text");
}
}
// JSXEmptyExpression is unique type since it doesn't actually parse anything,
// and so it should start at the end of last read token (left brace) and finish
// at the beginning of the next one (right brace).
jsxParseEmptyExpression(): N.JSXEmptyExpression {
const node = this.startNodeAt(this.state.lastTokEnd, this.state.lastTokEndLoc);
return this.finishNodeAt(node, "JSXEmptyExpression", this.state.start, this.state.startLoc);
}
// Parse JSX spread child
jsxParseSpreadChild(): N.JSXSpreadChild {
const node = this.startNode();
this.expect(tt.braceL);
this.expect(tt.ellipsis);
node.expression = this.parseExpression();
this.expect(tt.braceR);
return this.finishNode(node, "JSXSpreadChild");
}
// Parses JSX expression enclosed into curly brackets.
jsxParseExpressionContainer(): N.JSXExpressionContainer {
const node = this.startNode();
this.next();
if (this.match(tt.braceR)) {
node.expression = this.jsxParseEmptyExpression();
} else {
node.expression = this.parseExpression();
}
this.expect(tt.braceR);
return this.finishNode(node, "JSXExpressionContainer");
}
// Parses following JSX attribute name-value pair.
jsxParseAttribute(): N.JSXAttribute {
const node = this.startNode();
if (this.eat(tt.braceL)) {
this.expect(tt.ellipsis);
node.argument = this.parseMaybeAssign();
this.expect(tt.braceR);
return this.finishNode(node, "JSXSpreadAttribute");
}
node.name = this.jsxParseNamespacedName();
node.value = this.eat(tt.eq) ? this.jsxParseAttributeValue() : null;
return this.finishNode(node, "JSXAttribute");
}
// Parses JSX opening tag starting after "<".
jsxParseOpeningElementAt(startPos: number, startLoc: Position): N.JSXOpeningElement {
const node = this.startNodeAt(startPos, startLoc);
node.attributes = [];
node.name = this.jsxParseElementName();
while (!this.match(tt.slash) && !this.match(tt.jsxTagEnd)) {
node.attributes.push(this.jsxParseAttribute());
}
node.selfClosing = this.eat(tt.slash);
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXOpeningElement");
}
// Parses JSX closing tag starting after "</".
jsxParseClosingElementAt(startPos: number, startLoc: Position): N.JSXClosingElement {
const node = this.startNodeAt(startPos, startLoc);
node.name = this.jsxParseElementName();
this.expect(tt.jsxTagEnd);
return this.finishNode(node, "JSXClosingElement");
}
// Parses entire JSX element, including it"s opening tag
// (starting after "<"), attributes, contents and closing tag.
jsxParseElementAt(startPos: number, startLoc: Position): N.JSXElement {
const node = this.startNodeAt(startPos, startLoc);
const children = [];
const openingElement = this.jsxParseOpeningElementAt(startPos, startLoc);
let closingElement = null;
if (!openingElement.selfClosing) {
contents: for (;;) {
switch (this.state.type) {
case tt.jsxTagStart:
startPos = this.state.start; startLoc = this.state.startLoc;
this.next();
if (this.eat(tt.slash)) {
closingElement = this.jsxParseClosingElementAt(startPos, startLoc);
break contents;
}
children.push(this.jsxParseElementAt(startPos, startLoc));
break;
case tt.jsxText:
children.push(this.parseExprAtom());
break;
case tt.braceL:
if (this.lookahead().type === tt.ellipsis) {
children.push(this.jsxParseSpreadChild());
} else {
children.push(this.jsxParseExpressionContainer());
}
break;
// istanbul ignore next - should never happen
default:
throw this.unexpected();
}
}
// $FlowIgnore
if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
this.raise(
// $FlowIgnore
closingElement.start,
"Expected corresponding JSX closing tag for <" + getQualifiedJSXName(openingElement.name) + ">"
);
}
}
node.openingElement = openingElement;
node.closingElement = closingElement;
node.children = children;
if (this.match(tt.relational) && this.state.value === "<") {
this.raise(this.state.start, "Adjacent JSX elements must be wrapped in an enclosing tag");
}
return this.finishNode(node, "JSXElement");
}
// Parses entire JSX element from current position.
jsxParseElement(): N.JSXElement {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
this.next();
return this.jsxParseElementAt(startPos, startLoc);
}
// ==================================
// Overrides
// ==================================
parseExprAtom(refShortHandDefaultPos: ?Pos): N.Expression {
if (this.match(tt.jsxText)) {
return this.parseLiteral(this.state.value, "JSXText");
} else if (this.match(tt.jsxTagStart)) {
return this.jsxParseElement();
} else {
return super.parseExprAtom(refShortHandDefaultPos);
}
}
readToken(code: number): void {
if (this.state.inPropertyName) return super.readToken(code);
const context = this.curContext();
if (context === tc.j_expr) {
return this.jsxReadToken();
}
if (context === tc.j_oTag || context === tc.j_cTag) {
if (isIdentifierStart(code)) {
return this.jsxReadWord();
}
if (code === 62) {
++this.state.pos;
return this.finishToken(tt.jsxTagEnd);
}
if ((code === 34 || code === 39) && context === tc.j_oTag) {
return this.jsxReadString(code);
}
}
if (code === 60 && this.state.exprAllowed) {
++this.state.pos;
return this.finishToken(tt.jsxTagStart);
}
return super.readToken(code);
}
updateContext(prevType: TokenType): void {
if (this.match(tt.braceL)) {
const curContext = this.curContext();
if (curContext === tc.j_oTag) {
this.state.context.push(tc.braceExpression);
} else if (curContext === tc.j_expr) {
this.state.context.push(tc.templateQuasi);
} else {
super.updateContext(prevType);
}
this.state.exprAllowed = true;
} else if (this.match(tt.slash) && prevType === tt.jsxTagStart) {
this.state.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
this.state.context.push(tc.j_cTag); // reconsider as closing tag context
this.state.exprAllowed = false;
} else {
return super.updateContext(prevType);
}
}
};

View File

@@ -1,4 +1,6 @@
export default {
// @flow
const entities: { [name: string]: string } = {
quot: "\u0022",
amp: "&",
apos: "\u0027",
@@ -253,3 +255,4 @@ export default {
hearts: "\u2665",
diams: "\u2666"
};
export default entities;

View File

@@ -1,3 +1,5 @@
// @flow
// The algorithm used to determine whether a regexp can appear at a
// given point in the program is loosely based on sweet.js' approach.
// See https://github.com/mozilla/sweet.js/wiki/design

View File

@@ -1,19 +1,41 @@
/* eslint max-len: 0 */
// @flow
import type { TokenType } from "./types";
import type { Options } from "../options";
import type { Position } from "../util/location";
import { isIdentifierStart, isIdentifierChar, isKeyword } from "../util/identifier";
import { types as tt, keywords as keywordTypes } from "./types";
import { types as ct } from "./context";
import { type TokContext, types as ct } from "./context";
import LocationParser from "../parser/location";
import { SourceLocation } from "../util/location";
import { lineBreak, lineBreakG, isNewLine, nonASCIIwhitespace } from "../util/whitespace";
import State from "./state";
// The following character codes are forbidden from being
// an immediate sibling of NumericLiteralSeparator _
const forbiddenNumericLiteralSeparatorSiblings = [
46, // .
66, // B
69, // E
79, // O
88, // X
95, // _ (multiple separators are not allowed)
98, // b
101, // e
111, // o
120, // x
];
// Object type used to represent tokens. Note that normally, tokens
// simply exist as properties on the parser object. This is only
// used for the onToken callback and the external tokenizer.
export class Token {
constructor(state) {
constructor(state: State) {
this.type = state.type;
this.value = state.value;
this.start = state.start;
@@ -30,7 +52,7 @@ export class Token {
// ## Tokenizer
function codePointToString(code) {
function codePointToString(code: number): string {
// UTF-16 Decoding
if (code <= 0xFFFF) {
return String.fromCharCode(code);
@@ -39,15 +61,22 @@ function codePointToString(code) {
}
}
export default class Tokenizer {
constructor(options, input) {
export default class Tokenizer extends LocationParser {
// Forward-declarations
// parser/util.js
+unexpected: (pos?: ?number, messageOrType?: string | TokenType) => empty;
isLookahead: boolean;
constructor(options: Options, input: string) {
super();
this.state = new State;
this.state.init(options, input);
}
// Move to the next token
next() {
next(): void {
if (!this.isLookahead) {
this.state.tokens.push(new Token(this.state));
}
@@ -61,7 +90,7 @@ export default class Tokenizer {
// TODO
eat(type) {
eat(type: TokenType): boolean {
if (this.match(type)) {
this.next();
return true;
@@ -72,19 +101,19 @@ export default class Tokenizer {
// TODO
match(type) {
match(type: TokenType): boolean {
return this.state.type === type;
}
// TODO
isKeyword(word) {
isKeyword(word: string): boolean {
return isKeyword(word);
}
// TODO
lookahead() {
lookahead(): State {
const old = this.state;
this.state = old.clone(true);
@@ -100,7 +129,7 @@ export default class Tokenizer {
// Toggle strict mode. Re-reads the next number or string to please
// pedantic tests (`"use strict"; 010;` should fail).
setStrict(strict) {
setStrict(strict: boolean): void {
this.state.strict = strict;
if (!this.match(tt.num) && !this.match(tt.string)) return;
this.state.pos = this.state.start;
@@ -111,14 +140,14 @@ export default class Tokenizer {
this.nextToken();
}
curContext() {
curContext(): TokContext {
return this.state.context[this.state.context.length - 1];
}
// Read a single token, updating the parser object's token-related
// properties.
nextToken() {
nextToken(): void {
const curContext = this.curContext();
if (!curContext || !curContext.preserveSpace) this.skipSpace();
@@ -135,7 +164,7 @@ export default class Tokenizer {
}
}
readToken(code) {
readToken(code: number): void {
// Identifier or keyword. '\uXXXX' sequences are allowed in
// identifiers, so '\' also dispatches to that.
if (isIdentifierStart(code) || code === 92 /* '\' */) {
@@ -145,7 +174,7 @@ export default class Tokenizer {
}
}
fullCharCodeAtPos() {
fullCharCodeAtPos(): number {
const code = this.input.charCodeAt(this.state.pos);
if (code <= 0xd7ff || code >= 0xe000) return code;
@@ -153,7 +182,7 @@ export default class Tokenizer {
return (code << 10) + next - 0x35fdc00;
}
pushComment(block, text, start, end, startLoc, endLoc) {
pushComment(block: boolean, text: string, start: number, end: number, startLoc: Position, endLoc: Position): void {
const comment = {
type: block ? "CommentBlock" : "CommentLine",
value: text,
@@ -169,7 +198,7 @@ export default class Tokenizer {
}
}
skipBlockComment() {
skipBlockComment(): void {
const startLoc = this.state.curPosition();
const start = this.state.pos;
const end = this.input.indexOf("*/", this.state.pos += 2);
@@ -186,7 +215,7 @@ export default class Tokenizer {
this.pushComment(true, this.input.slice(start + 2, end), start, this.state.pos, startLoc, this.state.curPosition());
}
skipLineComment(startSkip) {
skipLineComment(startSkip: number): void {
const start = this.state.pos;
const startLoc = this.state.curPosition();
let ch = this.input.charCodeAt(this.state.pos += startSkip);
@@ -201,7 +230,7 @@ export default class Tokenizer {
// Called at the start of the parse and after every token. Skips
// whitespace and comments, and.
skipSpace() {
skipSpace(): void {
loop: while (this.state.pos < this.input.length) {
const ch = this.input.charCodeAt(this.state.pos);
switch (ch) {
@@ -250,7 +279,7 @@ export default class Tokenizer {
// the token, so that the next one's `start` will point at the
// right position.
finishToken(type, val) {
finishToken(type: TokenType, val: any): void {
this.state.end = this.state.pos;
this.state.endLoc = this.state.curPosition();
const prevType = this.state.type;
@@ -269,7 +298,7 @@ export default class Tokenizer {
//
// All in the name of speed.
//
readToken_dot() {
readToken_dot(): void {
const next = this.input.charCodeAt(this.state.pos + 1);
if (next >= 48 && next <= 57) {
return this.readNumber(true);
@@ -285,7 +314,7 @@ export default class Tokenizer {
}
}
readToken_slash() { // '/'
readToken_slash(): void { // '/'
if (this.state.exprAllowed) {
++this.state.pos;
return this.readRegexp();
@@ -299,7 +328,7 @@ export default class Tokenizer {
}
}
readToken_mult_modulo(code) { // '%*'
readToken_mult_modulo(code: number): void { // '%*'
let type = code === 42 ? tt.star : tt.modulo;
let width = 1;
let next = this.input.charCodeAt(this.state.pos + 1);
@@ -318,7 +347,7 @@ export default class Tokenizer {
return this.finishOp(type, width);
}
readToken_pipe_amp(code) { // '|&'
readToken_pipe_amp(code: number): void { // '|&'
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === code) return this.finishOp(code === 124 ? tt.logicalOR : tt.logicalAND, 2);
if (next === 61) return this.finishOp(tt.assign, 2);
@@ -326,7 +355,7 @@ export default class Tokenizer {
return this.finishOp(code === 124 ? tt.bitwiseOR : tt.bitwiseAND, 1);
}
readToken_caret() { // '^'
readToken_caret(): void { // '^'
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === 61) {
return this.finishOp(tt.assign, 2);
@@ -335,7 +364,7 @@ export default class Tokenizer {
}
}
readToken_plus_min(code) { // '+-'
readToken_plus_min(code: number): void { // '+-'
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === code) {
@@ -355,7 +384,7 @@ export default class Tokenizer {
}
}
readToken_lt_gt(code) { // '<>'
readToken_lt_gt(code: number): void { // '<>'
const next = this.input.charCodeAt(this.state.pos + 1);
let size = 1;
@@ -381,7 +410,7 @@ export default class Tokenizer {
return this.finishOp(tt.relational, size);
}
readToken_eq_excl(code) { // '=!'
readToken_eq_excl(code: number): void { // '=!'
const next = this.input.charCodeAt(this.state.pos + 1);
if (next === 61) return this.finishOp(tt.equality, this.input.charCodeAt(this.state.pos + 2) === 61 ? 3 : 2);
if (code === 61 && next === 62) { // '=>'
@@ -403,10 +432,19 @@ export default class Tokenizer {
}
}
getTokenFromCode(code) {
getTokenFromCode(code: number): void {
switch (code) {
case 35: // '#'
if (this.hasPlugin("classPrivateProperties") && this.state.inClass) {
++this.state.pos; return this.finishToken(tt.hash);
} else {
this.raise(this.state.pos, `Unexpected character '${codePointToString(code)}'`);
}
// The interpretation of a dot depends on whether it is followed
// by a digit or another two dots.
case 46: // '.'
return this.readToken_dot();
@@ -491,13 +529,13 @@ export default class Tokenizer {
this.raise(this.state.pos, `Unexpected character '${codePointToString(code)}'`);
}
finishOp(type, size) {
finishOp(type: TokenType, size: number): void {
const str = this.input.slice(this.state.pos, this.state.pos + size);
this.state.pos += size;
return this.finishToken(type, str);
}
readRegexp() {
readRegexp(): void {
const start = this.state.pos;
let escaped, inClass;
for (;;) {
@@ -539,13 +577,30 @@ export default class Tokenizer {
// were read, the integer value otherwise. When `len` is given, this
// will return `null` unless the integer has exactly `len` digits.
readInt(radix, len) {
readInt(radix: number, len?: number): number | null {
const start = this.state.pos;
let total = 0;
for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
const code = this.input.charCodeAt(this.state.pos);
let val;
if (this.hasPlugin("numericSeparator")) {
const prev = this.input.charCodeAt(this.state.pos - 1);
const next = this.input.charCodeAt(this.state.pos + 1);
if (code === 95) {
if ((forbiddenNumericLiteralSeparatorSiblings.indexOf(prev) > -1) ||
(forbiddenNumericLiteralSeparatorSiblings.indexOf(next) > -1) ||
Number.isNaN(next)) {
this.raise(this.state.pos, "Invalid NumericLiteralSeparator");
}
// Ignore this _ character
++this.state.pos;
continue;
}
}
if (code >= 97) {
val = code - 97 + 10; // a
} else if (code >= 65) {
@@ -564,7 +619,7 @@ export default class Tokenizer {
return total;
}
readRadixNumber(radix) {
readRadixNumber(radix: number): void {
this.state.pos += 2; // 0x
const val = this.readInt(radix);
if (val == null) this.raise(this.state.start + 2, "Expected number in radix " + radix);
@@ -574,32 +629,36 @@ export default class Tokenizer {
// Read an integer, octal integer, or floating-point number.
readNumber(startsWithDot) {
readNumber(startsWithDot: boolean): void {
const start = this.state.pos;
const firstIsZero = this.input.charCodeAt(start) === 48; // '0'
let octal = this.input.charCodeAt(start) === 48; // '0'
let isFloat = false;
if (!startsWithDot && this.readInt(10) === null) this.raise(start, "Invalid number");
if (octal && this.state.pos == start + 1) octal = false; // number === 0
let next = this.input.charCodeAt(this.state.pos);
if (next === 46) { // '.'
if (next === 46 && !octal) { // '.'
++this.state.pos;
this.readInt(10);
isFloat = true;
next = this.input.charCodeAt(this.state.pos);
}
if (next === 69 || next === 101) { // 'eE'
if ((next === 69 || next === 101) && !octal) { // 'eE'
next = this.input.charCodeAt(++this.state.pos);
if (next === 43 || next === 45) ++this.state.pos; // '+-'
if (this.readInt(10) === null) this.raise(start, "Invalid number");
isFloat = true;
}
if (isIdentifierStart(this.fullCharCodeAtPos())) this.raise(this.state.pos, "Identifier directly after number");
const str = this.input.slice(start, this.state.pos);
const str = this.input.slice(start, this.state.pos).replace(/_/g, "");
let val;
if (isFloat) {
val = parseFloat(str);
} else if (!firstIsZero || str.length === 1) {
} else if (!octal || str.length === 1) {
val = parseInt(str, 10);
} else if (this.state.strict) {
this.raise(start, "Invalid number");
@@ -613,7 +672,7 @@ export default class Tokenizer {
// Read a string value, interpreting backslash-escapes.
readCodePoint(throwOnInvalid) {
readCodePoint(throwOnInvalid: boolean): number | null {
const ch = this.input.charCodeAt(this.state.pos);
let code;
@@ -622,6 +681,7 @@ export default class Tokenizer {
code = this.readHexChar(this.input.indexOf("}", this.state.pos) - this.state.pos, throwOnInvalid);
++this.state.pos;
if (code === null) {
// $FlowFixMe (is this always non-null?)
--this.state.invalidTemplateEscapePosition; // to point to the '\'' instead of the 'u'
} else if (code > 0x10FFFF) {
if (throwOnInvalid) {
@@ -637,7 +697,7 @@ export default class Tokenizer {
return code;
}
readString(quote) {
readString(quote: number): void {
let out = "", chunkStart = ++this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) this.raise(this.state.start, "Unterminated string constant");
@@ -645,6 +705,7 @@ export default class Tokenizer {
if (ch === quote) break;
if (ch === 92) { // '\'
out += this.input.slice(chunkStart, this.state.pos);
// $FlowFixMe
out += this.readEscapedChar(false);
chunkStart = this.state.pos;
} else {
@@ -658,7 +719,7 @@ export default class Tokenizer {
// Reads template string tokens.
readTmplToken() {
readTmplToken(): void {
let out = "", chunkStart = this.state.pos, containsInvalid = false;
for (;;) {
if (this.state.pos >= this.input.length) this.raise(this.state.start, "Unterminated template");
@@ -709,7 +770,7 @@ export default class Tokenizer {
// Used to read escaped characters
readEscapedChar(inTemplate) {
readEscapedChar(inTemplate: boolean): string | null {
const throwOnInvalid = !inTemplate;
const ch = this.input.charCodeAt(++this.state.pos);
++this.state.pos;
@@ -736,6 +797,7 @@ export default class Tokenizer {
default:
if (ch >= 48 && ch <= 55) {
const codePos = this.state.pos - 1;
// $FlowFixMe
let octalStr = this.input.substr(this.state.pos - 1, 3).match(/^[0-7]+/)[0];
let octal = parseInt(octalStr, 8);
if (octal > 255) {
@@ -764,7 +826,7 @@ export default class Tokenizer {
// Used to read character escape sequences ('\x', '\u').
readHexChar(len, throwOnInvalid) {
readHexChar(len: number, throwOnInvalid: boolean): number | null {
const codePos = this.state.pos;
const n = this.readInt(16, len);
if (n === null) {
@@ -784,7 +846,7 @@ export default class Tokenizer {
// Incrementally adds only escaped chars, adding other chunks as-is
// as a micro-optimization.
readWord1() {
readWord1(): string {
this.state.containsEsc = false;
let word = "", first = true, chunkStart = this.state.pos;
while (this.state.pos < this.input.length) {
@@ -803,10 +865,12 @@ export default class Tokenizer {
++this.state.pos;
const esc = this.readCodePoint(true);
// $FlowFixMe (thinks esc may be null, but throwOnInvalid is true)
if (!(first ? isIdentifierStart : isIdentifierChar)(esc, true)) {
this.raise(escStart, "Invalid Unicode escape");
}
// $FlowFixMe
word += codePointToString(esc);
chunkStart = this.state.pos;
} else {
@@ -820,7 +884,7 @@ export default class Tokenizer {
// Read an identifier or keyword token. Will check for reserved
// words when necessary.
readWord() {
readWord(): void {
const word = this.readWord1();
let type = tt.name;
if (!this.state.containsEsc && this.isKeyword(word)) {
@@ -829,7 +893,7 @@ export default class Tokenizer {
return this.finishToken(type, word);
}
braceIsBlock(prevType) {
braceIsBlock(prevType: TokenType): boolean {
if (prevType === tt.colon) {
const parent = this.curContext();
if (parent === ct.braceStatement || parent === ct.braceExpression) {
@@ -852,7 +916,7 @@ export default class Tokenizer {
return !this.state.exprAllowed;
}
updateContext(prevType) {
updateContext(prevType: TokenType): void {
const type = this.state.type;
let update;

View File

@@ -1,11 +1,17 @@
// @flow
import type { Options } from "../options";
import * as N from "../types";
import type { TokContext } from "./context";
import type { Token } from "./index";
import type { TokenType } from "./types";
import { Position } from "../util/location";
import { types as ct } from "./context";
import { types as tt } from "./types";
export default class State {
init(options: Object, input: string) {
init(options: Options, input: string): void {
this.strict = options.strictMode === false ? false : options.sourceType === "module";
this.input = input;
@@ -18,6 +24,8 @@ export default class State {
this.inAsync =
this.inPropertyName =
this.inType =
this.inClass =
this.inClassProperty =
this.noAnonFunctionType =
false;
@@ -41,6 +49,7 @@ export default class State {
this.start = this.end = this.pos;
this.startLoc = this.endLoc = this.curPosition();
// $FlowIgnore
this.lastTokEndLoc = this.lastTokStartLoc = null;
this.lastTokStart = this.lastTokEnd = this.pos;
@@ -53,8 +62,6 @@ export default class State {
this.invalidTemplateEscapePosition = null;
this.exportedIdentifiers = [];
return this;
}
// TODO
@@ -69,27 +76,35 @@ export default class State {
// Flags to track whether we are in a function, a generator.
inFunction: boolean;
inGenerator: boolean;
inMethod: boolean;
inMethod: boolean | N.MethodKind;
inAsync: boolean;
inType: boolean;
noAnonFunctionType: boolean;
inPropertyName: boolean;
inClassProperty: boolean;
inClass: boolean;
// Labels in scope.
labels: Array<Object>;
labels: Array<{ kind: ?("loop" | "switch"), statementStart?: number }>;
// Leading decorators.
decorators: Array<Object>;
decorators: Array<N.Decorator>;
// Token store.
tokens: Array<Object>;
tokens: Array<Token | N.Comment>;
// Comment store.
comments: Array<Object>;
comments: Array<N.Comment>;
// Comment attachment store
trailingComments: Array<Object>;
leadingComments: Array<Object>;
commentStack: Array<Object>;
trailingComments: Array<N.Comment>;
leadingComments: Array<N.Comment>;
commentStack: Array<{
start: number;
leadingComments: ?Array<N.Comment>;
trailingComments: ?Array<N.Comment>;
}>;
commentPreviousNode: N.Node;
// The current position of the tokenizer in the input.
pos: number;
@@ -113,8 +128,8 @@ export default class State {
endLoc: Position;
// Position information for the previous token
lastTokEndLoc: ?Position;
lastTokStartLoc: ?Position;
lastTokEndLoc: Position;
lastTokStartLoc: Position;
lastTokStart: number;
lastTokEnd: number;
@@ -137,19 +152,23 @@ export default class State {
// `export default foo;` and `export { foo as default };`.
exportedIdentifiers: Array<string>;
curPosition() {
invalidTemplateEscapePosition: ?number;
curPosition(): Position {
return new Position(this.curLine, this.pos - this.lineStart);
}
clone(skipArrays?) {
clone(skipArrays?: boolean): State {
const state = new State;
for (const key in this) {
// $FlowIgnore
let val = this[key];
if ((!skipArrays || key === "context") && Array.isArray(val)) {
val = val.slice();
}
// $FlowIgnore
state[key] = val;
}
return state;

View File

@@ -1,3 +1,5 @@
// @flow
// ## Token types
// The assignment of fine-grained, information-carrying type objects
@@ -23,8 +25,33 @@ const isAssign = true;
const prefix = true;
const postfix = true;
type TokenOptions = {
keyword?: string;
beforeExpr?: boolean;
startsExpr?: boolean;
rightAssociative?: boolean;
isLoop?: boolean;
isAssign?: boolean;
prefix?: boolean;
postfix?: boolean;
binop?: ?number;
};
export class TokenType {
constructor(label, conf = {}) {
label: string;
keyword: ?string;
beforeExpr: boolean;
startsExpr: boolean;
rightAssociative: boolean;
isLoop: boolean;
isAssign: boolean;
prefix: boolean;
postfix: boolean;
binop: ?number;
updateContext: ?((prevType: TokenType) => void);
constructor(label: string, conf: TokenOptions = {}) {
this.label = label;
this.keyword = conf.keyword;
this.beforeExpr = !!conf.beforeExpr;
@@ -40,7 +67,7 @@ export class TokenType {
}
class KeywordTokenType extends TokenType {
constructor(name, options = {}) {
constructor(name: string, options: TokenOptions = {}) {
options.keyword = name;
super(name, options);
@@ -48,12 +75,12 @@ class KeywordTokenType extends TokenType {
}
export class BinopTokenType extends TokenType {
constructor(name, prec) {
constructor(name: string, prec: number) {
super(name, { beforeExpr, binop: prec });
}
}
export const types = {
export const types: { [name: string]: TokenType } = {
num: new TokenType("num", { startsExpr }),
regexp: new TokenType("regexp", { startsExpr }),
string: new TokenType("string", { startsExpr }),
@@ -82,6 +109,7 @@ export const types = {
backQuote: new TokenType("`", { startsExpr }),
dollarBraceL: new TokenType("${", { beforeExpr, startsExpr }),
at: new TokenType("@"),
hash: new TokenType("#"),
// Operators. These carry several kinds of properties to help the
// parser use them properly (the presence of these properties is

726
src/types.js Normal file
View File

@@ -0,0 +1,726 @@
// @flow
import type { Token } from "./tokenizer";
import type { SourceLocation } from "./util/location";
/*
* If making any changes to the AST, update:
* - This repository:
* - This file
* - `ast` directory
* - Babel repository:
* - packages/babel-types/src/definitions
* - packages/babel-generators/src/generators
*/
export type Comment = {
type: "CommentBlock" | "CommentLine";
value: string;
start: number;
end: number;
loc: SourceLocation;
};
export interface NodeBase {
start: number;
end: number;
loc: SourceLocation;
range: [number, number];
leadingComments?: ?Array<Comment>;
trailingComments?: ?Array<Comment>;
innerComments?: ?Array<Comment>;
extra: { [key: string]: any };
}
// Using a union type for `Node` makes type-checking too slow.
// Instead, add an index signature to allow a Node to be treated as anything.
export type Node = NodeBase & { [key: string]: any };
export type Expression = Node;
export type Statement = Node;
export type Pattern =
| Identifier
| ObjectPattern
| ArrayPattern
| RestElement
| AssignmentPattern;
export type Declaration =
| VariableDeclaration
| ClassDeclaration
| FunctionDeclaration;
export type DeclarationBase = NodeBase;
// TODO: Not in spec
export type HasDecorators = NodeBase & {
decorators?: $ReadOnlyArray<Decorator>;
};
export type Identifier = PatternBase & {
type: "Identifier";
name: string;
__clone(): Identifier;
};
export type PrivateName = NodeBase & {
type: "PrivateName";
name: string;
};
// Literals
export type Literal = RegExpLiteral | NullLiteral | StringLiteral | BooleanLiteral | NumericLiteral;
export type RegExpLiteral = NodeBase & {
type: "RegExpLiteral";
pattern: string;
flags: RegExp$flags;
};
export type NullLiteral = NodeBase & {
type: "NullLiteral";
}
export type StringLiteral = NodeBase & {
type: "StringLiteral";
value: string;
};
export type BooleanLiteral = NodeBase & {
type: "BooleanLiteral";
value: boolean;
};
export type NumericLiteral = NodeBase & {
type: "NumericLiteral";
value: number;
};
// Programs
export type BlockStatementLike = Program | BlockStatement;
export type File = NodeBase & {
type: "File";
program: Program;
comments: $ReadOnlyArray<Comment>;
tokens: $ReadOnlyArray<Token | Comment>;
};
export type Program = NodeBase & {
type: "Program";
sourceType: "script" | "module";
body: Array<Statement | ModuleDeclaration>; // TODO: $ReadOnlyArray
directives: $ReadOnlyArray<Directive>; // TODO: Not in spec
};
// Functions
export type Function =
NormalFunction | ArrowFunctionExpression | ObjectMethod | ClassMethod;
export type NormalFunction =
FunctionDeclaration | FunctionExpression;
export type FunctionBase = HasDecorators & {
id: ?Identifier;
params: $ReadOnlyArray<Pattern>;
body: BlockStatement;
generator: boolean;
async: boolean;
expression: boolean; // TODO: Not in spec
typeParameters?: ?FlowTypeParameterDeclaration; // TODO: Not in spec
returnType?: ?FlowTypeAnnotation; // TODO: Not in spec
};
// Statements
export type ExpressionStatement = NodeBase & {
type: "ExpressionStatement";
expression: Expression;
};
export type BlockStatement = NodeBase & {
type: "BlockStatement";
body: Array<Statement>; // TODO: $ReadOnlyArray
directives: $ReadOnlyArray<Directive>;
};
export type EmptyStatement = NodeBase & {
type: "EmptyStatement"
};
export type DebuggerStatement = NodeBase & {
type: "DebuggerStatement"
};
export type WithStatement = NodeBase & {
type: "WithStatement";
object: Expression;
body: Statement;
};
export type ReturnStatement = NodeBase & {
type: "ReturnStatement";
argument: ?Expression;
};
export type LabeledStatement = NodeBase & {
type: "LabeledStatement";
label: Identifier;
body: Statement;
};
export type BreakStatement = NodeBase & {
type: "BreakStatement";
label: ?Identifier;
};
export type ContinueStatement = NodeBase & {
type: "ContinueStatement";
label: ?Identifier;
};
// Choice
export type IfStatement = NodeBase & {
type: "IfStatement";
test: Expression;
consequent: Statement;
alternate: ?Statement;
};
export type SwitchStatement = NodeBase & {
type: "SwitchStatement";
discriminant: Expression;
cases: $ReadOnlyArray<SwitchCase>;
};
export type SwitchCase = NodeBase & {
type: "SwitchCase";
test: ?Expression;
consequent: $ReadOnlyArray<Statement>;
};
// Exceptions
export type ThrowStatement = NodeBase & {
type: "ThrowStatement";
argument: Expression;
};
export type TryStatement = NodeBase & {
type: "TryStatement";
block: BlockStatement;
handler: CatchClause | null;
finalizer: BlockStatement | null;
guardedHandlers: $ReadOnlyArray<empty>; // TODO: Not in spec
};
export type CatchClause = NodeBase & {
type: "CatchClause";
param: Pattern;
body: BlockStatement;
};
// Loops
export type WhileStatement = NodeBase & {
type: "WhileStatement";
test: Expression;
body: Statement;
};
export type DoWhileStatement = NodeBase & {
type: "DoWhileStatement";
body: Statement;
test: Expression;
};
export type ForLike = ForStatement | ForInOf;
export type ForStatement = NodeBase & {
type: "ForStatement";
init: ?(VariableDeclaration | Expression);
test: ?Expression;
update: ?Expression;
body: Statement;
};
export type ForInOf = ForInStatement | ForOfStatement;
export type ForInOfBase = NodeBase & {
type: "ForInStatement";
left: VariableDeclaration | Expression;
right: Expression;
body: Statement;
};
export type ForInStatement = ForInOfBase & {
type: "ForInStatement";
// TODO: Shouldn't be here, but have to declare it because it's assigned to a ForInOf unconditionally.
await: boolean;
};
export type ForOfStatement = ForInOfBase & {
type: "ForOfStatement";
await: boolean;
};
// Declarations
export type OptFunctionDeclaration = FunctionBase & DeclarationBase & HasDecorators & {
type: "FunctionDeclaration";
};
export type FunctionDeclaration = OptFunctionDeclaration & {
id: Identifier;
}
export type VariableDeclaration = DeclarationBase & HasDecorators & {
type: "VariableDeclaration";
declarations: $ReadOnlyArray<VariableDeclarator>;
kind: "var" | "let" | "const";
};
export type VariableDeclarator = NodeBase & {
type: "VariableDeclarator";
id: Pattern;
init: ?Expression;
};
// Misc
export type Decorator = NodeBase & {
type: "Decorator";
expression: Expression;
};
export type Directive = NodeBase & {
type: "Directive";
value: DirectiveLiteral;
};
export type DirectiveLiteral = StringLiteral & { type: "DirectiveLiteral" };
// Expressions
export type Super = NodeBase & { type: "Super" };
export type Import = NodeBase & { type: "Import" };
export type ThisExpression = NodeBase & { type: "ThisExpression" };
export type ArrowFunctionExpression = FunctionBase & {
type: "ArrowFunctionExpression";
body: BlockStatement | Expression;
};
export type YieldExpression = NodeBase & {
type: "YieldExpression";
argument: ?Expression;
delegate: boolean;
};
export type AwaitExpression = NodeBase & {
type: "AwaitExpression";
argument: ?Expression;
};
export type ArrayExpression = NodeBase & {
type: "ArrayExpression";
elements: $ReadOnlyArray<?(Expression | SpreadElement)>;
};
export type ObjectExpression = NodeBase & {
type: "ObjectExpression";
properties: $ReadOnlyArray<ObjectProperty | ObjectMethod | SpreadElement>;
};
export type ObjectOrClassMember = ClassMethod | ClassProperty | ClassPrivateProperty | ObjectMember;
export type ObjectMember = ObjectProperty | ObjectMethod;
export type ObjectMemberBase = NodeBase & {
key: Expression;
computed: boolean;
value: Expression;
decorators: $ReadOnlyArray<Decorator>;
kind?: "get" | "set" | "method";
method: boolean; // TODO: Not in spec
variance?: ?FlowVariance; // TODO: Not in spec
};
export type ObjectProperty = ObjectMemberBase & {
type: "ObjectProperty";
shorthand: boolean;
};
export type ObjectMethod = ObjectMemberBase & MethodBase & {
type: "ObjectMethod";
kind: "get" | "set" | "method"; // Never "constructor"
};
export type FunctionExpression = MethodBase & {
kind?: void; // never set
type: "FunctionExpression";
};
// Unary operations
export type UnaryExpression = NodeBase & {
type: "UnaryExpression";
operator: UnaryOperator;
prefix: boolean;
argument: Expression;
};
export type UnaryOperator = "-" | "+" | "!" | "~" | "typeof" | "void" | "delete";
export type UpdateExpression = NodeBase & {
type: "UpdateExpression";
operator: UpdateOperator;
argument: Expression;
prefix: boolean;
};
export type UpdateOperator = "++" | "--";
// Binary operations
export type BinaryExpression = NodeBase & {
type: "BinaryExpression";
operator: BinaryOperator;
left: Expression;
right: Expression;
};
export type BinaryOperator =
| "==" | "!=" | "===" | "!=="
| "<" | "<=" | ">" | ">="
| "<<" | ">>" | ">>>"
| "+" | "-" | "*" | "/" | "%"
| "|" | "^" | "&" | "in"
| "instanceof";
export type AssignmentExpression = NodeBase & {
type: "AssignmentExpression";
operator: AssignmentOperator;
left: Pattern | Expression;
right: Expression;
};
export type AssignmentOperator =
| "=" | "+=" | "-=" | "*=" | "/=" | "%="
| "<<=" | ">>=" | ">>>="
| "|=" | "^=" | "&=";
export type LogicalExpression = NodeBase & {
type: "LogicalExpression";
operator: LogicalOperator;
left: Expression;
right: Expression;
};
export type LogicalOperator = "||" | "&&";
export type SpreadElement = NodeBase & {
type: "SpreadElement";
argument: Expression;
};
export type MemberExpression = NodeBase & {
type: "MemberExpression";
object: Expression | Super;
property: Expression;
computed: boolean;
}
export type BindExpression = NodeBase & {
type: "BindExpression";
object: $ReadOnlyArray<?Expression>;
callee: $ReadOnlyArray<Expression>;
};
export type ConditionalExpression = NodeBase & {
type: "ConditionalExpression";
test: Expression;
alternate: Expression;
consequent: Expression;
};
export type CallOrNewBase = NodeBase & {
callee: Expression | Super | Import;
arguments: Array<Expression | SpreadElement>; // TODO: $ReadOnlyArray
};
export type CallExpression = CallOrNewBase & {
type: "CallExpression";
};
export type NewExpression = CallOrNewBase & {
type: "NewExpression";
};
export type SequenceExpression = NodeBase & {
type: "SequenceExpression";
expressions: $ReadOnlyArray<Expression>;
};
// Template Literals
export type TemplateLiteral = NodeBase & {
type: "TemplateLiteral";
quasis: $ReadOnlyArray<TemplateElement>;
expressions: $ReadOnlyArray<Expression>;
};
export type TaggedTmplateExpression = NodeBase & {
type: "TaggedTemplateExpression";
tag: Expression;
quasi: TemplateLiteral;
};
export type TemplateElement = NodeBase & {
type: "TemplateElement";
tail: boolean;
value: {
cooked: string;
raw: string;
}
};
// Patterns
export type PatternBase = HasDecorators & {
// Flow only:
optional?: true;
typeAnnotation?: ?FlowTypeAnnotation;
};
export type AssignmentProperty = ObjectProperty & {
value: Pattern;
};
export type ObjectPattern = PatternBase & {
type: "ObjectPattern";
properties: $ReadOnlyArray<AssignmentProperty | RestElement>;
};
export type ArrayPattern = PatternBase & {
type: "ArrayPattern";
elements: $ReadOnlyArray<?Pattern>;
};
export type RestElement = PatternBase & {
type: "RestElement";
argument: Pattern;
};
export type AssignmentPattern = PatternBase & {
type: "AssignmentPattern";
left: Pattern;
right: Expression;
};
// Classes
export type Class = ClassDeclaration | ClassExpression;
export type ClassBase = HasDecorators & {
id: ?Identifier;
superClass: ?Expression;
body: ClassBody;
decorators: $ReadOnlyArray<Decorator>;
typeParameters?: ?FlowTypeParameterDeclaration; // TODO: Not in spec
superTypeParameters?: ?FlowTypeParameterInstantiation; // TODO: Not in spec
implements?: $ReadOnlyArray<FlowClassImplements>;
};
export type ClassBody = NodeBase & {
type: "ClassBody";
body: Array<ClassMember>; // TODO: $ReadOnlyArray
};
export type ClassMemberBase = NodeBase & HasDecorators & {
static: boolean;
computed: boolean;
// TypeScript only:
access?: ?Accessibility;
abstract?: ?true;
optional?: ?true;
}
export type Accessibility = "public" | "protected" | "private";
export type ClassMember = ClassMethod | ClassProperty | ClassPrivateProperty;
export type MethodLike = ObjectMethod | FunctionExpression | ClassMethod;
export type MethodBase = FunctionBase & {
+kind?: MethodKind;
};
export type MethodKind = "constructor" | "method" | "get" | "set";
export type ClassMethod = MethodBase & ClassMemberBase & {
type: "ClassMethod";
key: Expression;
kind: MethodKind;
static: boolean;
decorators: $ReadOnlyArray<Decorator>;
variance?: ?FlowVariance; // TODO: Not in spec
};
export type ClassProperty = ClassMemberBase & {
type: "ClassProperty";
key: Identifier;
value: ?Expression; // TODO: Not in spec that this is nullable.
typeAnnotation?: ?FlowTypeAnnotation; // TODO: Not in spec
variance?: ?FlowVariance; // TODO: Not in spec
// TypeScript only: (TODO: Not in spec)
readonly?: true;
};
export type ClassPrivateProperty = NodeBase & {
type: "ClassPrivateProperty";
key: Identifier;
value: ?Expression; // TODO: Not in spec that this is nullable.
};
export type OptClassDeclaration = ClassBase & DeclarationBase & HasDecorators & {
type: "ClassDeclaration";
// TypeScript only
abstract?: ?true;
};
export type ClassDeclaration = OptClassDeclaration & {
id: Identifier;
};
export type ClassExpression = ClassBase & { type: "ClassExpression" };
export type MetaProperty = NodeBase & {
type: "MetaProperty";
meta: Identifier;
property: Identifier;
};
// Modules
export type ModuleDeclaration = AnyImport | AnyExport;
export type AnyImport = ImportDeclaration;
export type AnyExport =
| ExportNamedDeclaration
| ExportDefaultDeclaration
| ExportAllDeclaration;
export type ModuleSpecifier = NodeBase & {
local: Identifier;
};
// Imports
export type ImportDeclaration = NodeBase & {
type: "ImportDeclaration";
// TODO: $ReadOnlyArray
specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>;
source: Literal;
importKind?: "type" | "typeof" | "value"; // TODO: Not in spec
};
export type ImportSpecifier = ModuleSpecifier & {
type: "ImportSpecifier";
imported: Identifier;
};
export type ImportDefaultSpecifier = ModuleSpecifier & {
type: "ImportDefaultSpecifier"
};
export type ImportNamespaceSpecifier = ModuleSpecifier & {
type: "ImportNamespaceSpecifier"
};
// Exports
export type ExportNamedDeclaration = NodeBase & {
type: "ExportNamedDeclaration";
declaration: ?Declaration;
specifiers: $ReadOnlyArray<ExportSpecifier>;
source: ?Literal;
exportKind?: "type" | "value"; // TODO: Not in spec
};
export type ExportSpecifier = NodeBase & {
type: "ExportSpecifier";
exported: Identifier;
};
export type ExportDefaultDeclaration = NodeBase & {
type: "ExportDefaultDeclaration";
declaration: OptFunctionDeclaration | OptClassDeclaration | Expression;
};
export type ExportAllDeclaration = NodeBase & {
type: "ExportAllDeclaration";
source: Literal;
};
// JSX (TODO: Not in spec)
export type JSXIdentifier = Node;
export type JSXNamespacedName = Node;
export type JSXMemberExpression = Node;
export type JSXEmptyExpression = Node;
export type JSXSpreadChild = Node;
export type JSXExpressionContainer = Node;
export type JSXAttribute = Node;
export type JSXOpeningElement = Node;
export type JSXClosingElement = Node;
export type JSXElement = Node;
// Flow (TODO: Not in spec)
export type FlowType = Node;
export type FlowPredicate = Node;
export type FlowDeclare = Node;
export type FlowDeclareClass = Node;
export type FlowDeclareExportDeclaration = Node;
export type FlowDeclareFunction = Node;
export type FlowDeclareVariable = Node;
export type FlowDeclareModule = Node;
export type FlowDeclareModuleExports = Node;
export type FlowDeclareTypeAlias = Node;
export type FlowDeclareInterface = Node;
export type FlowInterface = Node;
export type FlowInterfaceExtends = Node;
export type FlowTypeAlias = Node;
export type FlowTypeParameter = Node;
export type FlowTypeParameterDeclaration = Node;
export type FlowTypeParameterInstantiation = Node;
export type FlowObjectTypeIndexer = Node;
export type FlowFunctionTypeAnnotation = Node;
export type FlowObjectTypeProperty = Node;
export type FlowObjectTypeSpreadProperty = Node;
export type FlowObjectTypeCallProperty = Node;
export type FlowObjectTypeAnnotation = Node;
export type FlowQualifiedTypeIdentifier = Node;
export type FlowGenericTypeAnnotation = Node;
export type FlowTypeofTypeAnnotation = Node;
export type FlowTupleTypeAnnotation = Node;
export type FlowFunctionTypeParam = Node;
export type FlowTypeAnnotation = Node;
export type FlowVariance = Node;
export type FlowClassImplements = Node;

View File

@@ -1,5 +1,7 @@
/* eslint max-len: 0 */
// @flow
// This is a trick taken from Esprima. It turns out that, on
// non-Chrome browsers, to check whether a string is in a set, a
// predicate containing a big ugly `switch` statement is faster than
@@ -9,17 +11,17 @@
//
// It starts by sorting the words by length.
function makePredicate(words) {
words = words.split(" ");
function makePredicate(words: string): (str: string) => boolean {
const wordsArr = words.split(" ");
return function (str) {
return words.indexOf(str) >= 0;
return wordsArr.indexOf(str) >= 0;
};
}
// Reserved word lists for various dialects of the language
export const reservedWords = {
6: makePredicate("enum await"),
"6": makePredicate("enum await"),
strict: makePredicate("implements interface let package private protected public static yield"),
strictBind: makePredicate("eval arguments")
};
@@ -57,7 +59,7 @@ const astralIdentifierCodes = [509,0,227,0,150,4,294,9,1368,2,2,1,6,3,41,2,5,0,1
// This has a complexity linear to the value of the code. The
// assumption is that looking up astral identifier characters is
// rare.
function isInAstralSet(code, set) {
function isInAstralSet(code: number, set: $ReadOnlyArray<number>): boolean {
let pos = 0x10000;
for (let i = 0; i < set.length; i += 2) {
pos += set[i];
@@ -66,11 +68,12 @@ function isInAstralSet(code, set) {
pos += set[i + 1];
if (pos >= code) return true;
}
return false;
}
// Test whether a given character code starts an identifier.
export function isIdentifierStart(code) {
export function isIdentifierStart(code: number): boolean {
if (code < 65) return code === 36;
if (code < 91) return true;
if (code < 97) return code === 95;
@@ -81,7 +84,7 @@ export function isIdentifierStart(code) {
// Test whether a given character is part of an identifier.
export function isIdentifierChar(code) {
export function isIdentifierChar(code: number): boolean {
if (code < 48) return code === 36;
if (code < 58) return true;
if (code < 65) return false;

View File

@@ -1,9 +1,18 @@
// @flow
import { lineBreakG } from "./whitespace";
export type Pos = {
start: number;
}
// These are used when `options.locations` is on, for the
// `startLoc` and `endLoc` properties.
export class Position {
line: number;
column: number;
constructor(line: number, col: number) {
this.line = line;
this.column = col;
@@ -11,8 +20,13 @@ export class Position {
}
export class SourceLocation {
start: Position;
end: Position;
filename: string;
constructor(start: Position, end?: Position) {
this.start = start;
// $FlowIgnore (may start as null, but initialized later)
this.end = end;
}
}
@@ -23,7 +37,7 @@ export class SourceLocation {
// offset. `input` should be the code string that the offset refers
// into.
export function getLineInfo(input, offset) {
export function getLineInfo(input: string, offset: number): Position {
for (let line = 1, cur = 0; ;) {
lineBreakG.lastIndex = cur;
const match = lineBreakG.exec(input);
@@ -34,4 +48,6 @@ export function getLineInfo(input, offset) {
return new Position(line, offset - cur);
}
}
// istanbul ignore next
throw new Error("Unreachable");
}

View File

@@ -1,3 +1,5 @@
// @flow
// Matches a whole line break (where CRLF is considered a single
// line break). Used to count lines.