Skip to content

Commit

Permalink
Fix several edge cases with context expression state
Browse files Browse the repository at this point in the history
  • Loading branch information
danez committed Nov 5, 2018
1 parent 1d4d760 commit d6cb7f5
Show file tree
Hide file tree
Showing 20 changed files with 2,003 additions and 31 deletions.
28 changes: 27 additions & 1 deletion packages/babel-parser/src/parser/expression.js
Expand Up @@ -950,7 +950,14 @@ export default class ExpressionParser extends LValParser {

parseFunctionExpression(): N.FunctionExpression | N.MetaProperty {
const node = this.startNode();
const meta = this.parseIdentifier(true);

// We do not do parseIdentifier here because of perf but more importantly
// because parseIdentifier will remove an item from the expression stack
// if function or class is parsed as identifier (in objects e.g.).
let meta = this.startNode();
this.next();
meta = this.createIdentifier(meta, "function");

if (this.state.inGenerator && this.eat(tt.dot)) {
return this.parseMetaProperty(node, meta, "sent");
}
Expand Down Expand Up @@ -1839,8 +1846,14 @@ export default class ExpressionParser extends LValParser {
parseIdentifier(liberal?: boolean): N.Identifier {
const node = this.startNode();
const name = this.parseIdentifierName(node.start, liberal);

return this.createIdentifier(node, name);
}

createIdentifier(node: N.Identifier, name: string): N.Identifier {
node.name = name;
node.loc.identifierName = name;

return this.finishNode(node, "Identifier");
}

Expand All @@ -1860,6 +1873,19 @@ export default class ExpressionParser extends LValParser {
name = this.state.value;
} else if (this.state.type.keyword) {
name = this.state.type.keyword;

// `class` and `function` keywords push new context into this.context.
// But there is no chance to pop the context if the keyword is consumed
// as an identifier such as a property name.
// If the previous token is a dot, this does not apply because the
// context-managing code already ignored the keyword
if (
(name === "class" || name === "function") &&
(this.state.lastTokEnd !== this.state.lastTokStart + 1 ||
this.input.charCodeAt(this.state.lastTokStart) !== 46)
) {
this.state.context.pop();
}
} else {
throw this.unexpected();
}
Expand Down
7 changes: 6 additions & 1 deletion packages/babel-parser/src/plugins/jsx/index.js
Expand Up @@ -538,7 +538,12 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
}

if (code === charCodes.lessThan && this.state.exprAllowed) {
if (
code === charCodes.lessThan &&
this.state.exprAllowed &&
this.state.input.charCodeAt(this.state.pos + 1) !==
charCodes.exclamationMark
) {
++this.state.pos;
return this.finishToken(tt.jsxTagStart);
}
Expand Down
86 changes: 66 additions & 20 deletions packages/babel-parser/src/tokenizer/context.js
Expand Up @@ -12,30 +12,48 @@ export class TokContext {
token: string,
isExpr?: boolean,
preserveSpace?: boolean,
override?: Function, // Takes a Tokenizer as a this-parameter, and returns void.
override?: ?Function, // Takes a Tokenizer as a this-parameter, and returns void.
generator?: boolean,
) {
this.token = token;
this.isExpr = !!isExpr;
this.preserveSpace = !!preserveSpace;
this.override = override;
this.generator = !!generator;
}

token: string;
isExpr: boolean;
preserveSpace: boolean;
override: ?Function;
generator: boolean;
}

export const types: {
[key: string]: TokContext,
} = {
braceStatement: new TokContext("{", false),
braceExpression: new TokContext("{", true),
templateQuasi: new TokContext("${", true),
templateQuasi: new TokContext("${", false),
parenStatement: new TokContext("(", false),
parenExpression: new TokContext("(", true),
template: new TokContext("`", true, true, p => p.readTmplToken()),
functionExpression: new TokContext("function", true),
functionStatement: new TokContext("function", false),
functionExpressionGenerator: new TokContext(
"function",
true,
false,
null,
true,
),
functionStatementGenerator: new TokContext(
"function",
false,
false,
null,
true,
),
};

// Token-specific context update code
Expand All @@ -46,30 +64,44 @@ tt.parenR.updateContext = tt.braceR.updateContext = function() {
return;
}

const out = this.state.context.pop();
if (
out === types.braceStatement &&
this.curContext() === types.functionExpression
) {
this.state.context.pop();
this.state.exprAllowed = false;
} else if (out === types.templateQuasi) {
this.state.exprAllowed = true;
} else {
this.state.exprAllowed = !out.isExpr;
let out = this.state.context.pop();
if (out === types.braceStatement && this.curContext().token === "function") {
out = this.state.context.pop();
}

this.state.exprAllowed = !out.isExpr;
};

tt.name.updateContext = function(prevType) {
if (this.state.value === "of" && this.curContext() === types.parenStatement) {
this.state.exprAllowed = !prevType.beforeExpr;
return;
tt.star.updateContext = function(prevType) {
if (prevType === tt._function) {
const index = this.state.context.length - 1;
if (this.state.context[index] === types.functionExpression) {
this.state.context[index] = types.functionExpressionGenerator;
} else {
this.state.context[index] = types.functionStatementGenerator;
}
}
this.state.exprAllowed = true;
};

this.state.exprAllowed = false;
tt.name.updateContext = function(prevType) {
let allowed = false;
if (prevType !== tt.dot) {
if (
(this.state.value === "of" && !this.state.exprAllowed) ||
(this.state.value === "yield" && this.inGeneratorContext())
) {
allowed = true;
}
}
this.state.exprAllowed = allowed;

if (prevType === tt._let || prevType === tt._const || prevType === tt._var) {
if (lineBreak.test(this.input.slice(this.state.end))) {
if (
lineBreak.test(
this.input.slice(this.state.end, this.lookahead(true).start),
)
) {
this.state.exprAllowed = true;
}
}
Expand Down Expand Up @@ -107,8 +139,22 @@ tt.incDec.updateContext = function() {
};

tt._function.updateContext = tt._class.updateContext = function(prevType) {
if (this.state.exprAllowed && !this.braceIsBlock(prevType)) {
if (
prevType.beforeExpr &&
prevType !== tt.semi &&
prevType !== tt._else &&
!(
prevType === tt._return &&
lineBreak.test(this.input.slice(this.state.lastTokEnd, this.state.start))
) &&
!(
(prevType === tt.colon || prevType === tt.braceL) &&
this.curContext() === types.b_stat
)
) {
this.state.context.push(types.functionExpression);
} else {
this.state.context.push(types.functionStatement);
}

this.state.exprAllowed = false;
Expand Down
44 changes: 36 additions & 8 deletions packages/babel-parser/src/tokenizer/index.js
Expand Up @@ -1325,14 +1325,25 @@ export default class Tokenizer extends LocationParser {
}

braceIsBlock(prevType: TokenType): boolean {
if (prevType === tt.colon) {
const parent = this.curContext();
if (parent === ct.braceStatement || parent === ct.braceExpression) {
return !parent.isExpr;
}
const parent = this.curContext();
if (parent === ct.functionExpression || parent === ct.functionStatement) {
return true;
}
if (
prevType === tt.colon &&
(parent === ct.braceStatement || parent === ct.braceExpression)
) {
return !parent.isExpr;
}

if (prevType === tt._return) {
// The check for `tt.name && exprAllowed` detects whether we are
// after a `yield` or `of` construct. See the `updateContext` for
// `tt.name`.
if (
prevType === tt._return ||
prevType === tt._yield ||
(prevType === tt.name && this.state.exprAllowed)
) {
return lineBreak.test(
this.input.slice(this.state.lastTokEnd, this.state.start),
);
Expand All @@ -1342,13 +1353,22 @@ export default class Tokenizer extends LocationParser {
prevType === tt._else ||
prevType === tt.semi ||
prevType === tt.eof ||
prevType === tt.parenR
prevType === tt.parenR ||
prevType === tt.arrow
) {
return true;
}

if (prevType === tt.braceL) {
return this.curContext() === ct.braceStatement;
return parent === ct.braceStatement;
}

if (
prevType === tt._var ||
prevType === tt._let ||
prevType === tt._const
) {
return false;
}

if (prevType === tt.relational) {
Expand All @@ -1359,6 +1379,14 @@ export default class Tokenizer extends LocationParser {
return !this.state.exprAllowed;
}

inGeneratorContext() {
for (let i = this.state.context.length - 1; i >= 1; i--) {
const context = this.state.context[i];
if (context.token === "function") return context.generator;
}
return false;
}

updateContext(prevType: TokenType): void {
const type = this.state.type;
let update;
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-parser/src/tokenizer/types.js
Expand Up @@ -174,7 +174,7 @@ export const keywords = {
new: new KeywordTokenType("new", { beforeExpr, startsExpr }),
this: new KeywordTokenType("this", { startsExpr }),
super: new KeywordTokenType("super", { startsExpr }),
class: new KeywordTokenType("class"),
class: new KeywordTokenType("class", { startsExpr }),
extends: new KeywordTokenType("extends", { beforeExpr }),
export: new KeywordTokenType("export"),
import: new KeywordTokenType("import", { startsExpr }),
Expand Down
@@ -0,0 +1 @@
for (const {a} of /b/) {}

0 comments on commit d6cb7f5

Please sign in to comment.