2019-03-05 00:45:42 +01:00

201 lines
5.1 KiB
JavaScript

// @flow
import { types as tt, type TokenType } from "../tokenizer/types";
import Tokenizer from "../tokenizer";
import type { Node } from "../types";
import { lineBreak, skipWhiteSpace } from "../util/whitespace";
const literal = /^('|")((?:\\?.)*?)\1/;
// ## Parser utilities
export default class UtilParser extends Tokenizer {
// TODO
addExtra(node: Node, key: string, val: any): void {
if (!node) return;
const extra = (node.extra = node.extra || {});
extra[key] = val;
}
// TODO
isRelational(op: "<" | ">"): boolean {
return this.match(tt.relational) && this.state.value === op;
}
isLookaheadRelational(op: "<" | ">"): boolean {
const l = this.lookahead();
return l.type === tt.relational && l.value === op;
}
// TODO
expectRelational(op: "<" | ">"): void {
if (this.isRelational(op)) {
this.next();
} else {
this.unexpected(null, tt.relational);
}
}
// eat() for relational operators.
eatRelational(op: "<" | ">"): boolean {
if (this.isRelational(op)) {
this.next();
return true;
}
return false;
}
// Tests whether parsed token is a contextual keyword.
isContextual(name: string): boolean {
return (
this.match(tt.name) &&
this.state.value === name &&
!this.state.containsEsc
);
}
isLookaheadContextual(name: string): boolean {
const l = this.lookahead();
return l.type === tt.name && l.value === name;
}
// Consumes contextual keyword if possible.
eatContextual(name: string): boolean {
return this.isContextual(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) ||
this.hasPrecedingLineBreak()
);
}
hasPrecedingLineBreak(): boolean {
return lineBreak.test(
this.state.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);
}
// Throws if the current token and the prev one are separated by a space.
assertNoSpace(message: string = "Unexpected space."): void {
if (this.state.start > this.state.lastTokEnd) {
this.raise(this.state.lastTokEnd, message);
}
}
// 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);
}
expectPlugin(name: string, pos?: ?number): true {
if (!this.hasPlugin(name)) {
throw this.raise(
pos != null ? pos : this.state.start,
`This experimental syntax requires enabling the parser plugin: '${name}'`,
{ missingPluginNames: [name] },
);
}
return true;
}
expectOnePlugin(names: Array<string>, pos?: ?number): void {
if (!names.some(n => this.hasPlugin(n))) {
throw this.raise(
pos != null ? pos : this.state.start,
`This experimental syntax requires enabling one of the following parser plugin(s): '${names.join(
", ",
)}'`,
{ missingPluginNames: names },
);
}
}
checkYieldAwaitInDefaultParams() {
if (
this.state.yieldPos &&
(!this.state.awaitPos || this.state.yieldPos < this.state.awaitPos)
) {
this.raise(
this.state.yieldPos,
"Yield cannot be used as name inside a generator function",
);
}
if (this.state.awaitPos) {
this.raise(
this.state.awaitPos,
"Await cannot be used as name inside an async function",
);
}
}
strictDirective(start: number): boolean {
for (;;) {
// Try to find string literal.
skipWhiteSpace.lastIndex = start;
// $FlowIgnore
start += skipWhiteSpace.exec(this.state.input)[0].length;
const match = literal.exec(this.state.input.slice(start));
if (!match) break;
if (match[2] === "use strict") return true;
start += match[0].length;
// Skip semicolon, if any.
skipWhiteSpace.lastIndex = start;
// $FlowIgnore
start += skipWhiteSpace.exec(this.state.input)[0].length;
if (this.state.input[start] === ";") {
start++;
}
}
return false;
}
}