Move plugin helpers out of Parser.prototype and into the plugin itself (#482)

This commit is contained in:
Andy 2017-04-23 15:34:35 -07:00 committed by Daniel Tschinder
parent 8601f2f7da
commit 2c1193b7da
3 changed files with 1256 additions and 1252 deletions

View File

@ -1,40 +1,4 @@
import { types as tt } from "../tokenizer/types";
import Parser from "../parser";
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 &&
@ -44,6 +8,44 @@ function isSimpleProperty(node) {
}
export default (superClass) => class extends superClass {
estreeParseRegExpLiteral({ 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;
}
estreeParseLiteral(value) {
return this.parseLiteral(value, "Literal");
}
directiveToStmt(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);
}
// ==================================
// Overrides
// ==================================
checkDeclaration(node) {
if (isSimpleProperty(node)) {
this.checkDeclaration(node.value);

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
import XHTMLEntities from "./xhtml";
import { TokenType, types as tt } from "../../tokenizer/types";
import { TokContext, types as tc } from "../../tokenizer/context";
import Parser from "../../parser";
import { isIdentifierChar, isIdentifierStart } from "../../util/identifier";
import { isNewLine } from "../../util/whitespace";
@ -33,144 +32,6 @@ 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) {
@ -187,215 +48,355 @@ function getQualifiedJSXName(object) {
}
}
// Parse next token as JSX identifier
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;
}
case tt.jsxTagStart:
case tt.string:
return this.parseExprAtom();
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;
}
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:
this.unexpected();
}
}
if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
this.raise(
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.
pp.jsxParseElement = function() {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
this.next();
return this.jsxParseElementAt(startPos, startLoc);
};
export default (superClass) => class extends superClass {
// Reads inline JSX contents token.
jsxReadToken() {
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;
}
}
}
}
jsxReadNewLine(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;
}
jsxReadString(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);
}
jsxReadEntity() {
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.
jsxReadWord() {
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() {
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() {
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() {
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() {
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;
}
case tt.jsxTagStart:
case tt.string:
return this.parseExprAtom();
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).
jsxParseEmptyExpression() {
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() {
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() {
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() {
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, 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 "</".
jsxParseClosingElementAt(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.
jsxParseElementAt(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;
}
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:
this.unexpected();
}
}
if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
this.raise(
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() {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
this.next();
return this.jsxParseElementAt(startPos, startLoc);
}
// ==================================
// Overrides
// ==================================
parseExprAtom(refShortHandDefaultPos) {
if (this.match(tt.jsxText)) {
return this.parseLiteral(this.state.value, "JSXText");