Skip to content
This repository has been archived by the owner on May 19, 2018. It is now read-only.

Optional Chaining: Stage 1 plugin #545

Merged
merged 17 commits into from
Jun 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ require("babylon").parse("code", {
- `functionSent`
- `dynamicImport` ([proposal](https://github.com/tc39/proposal-dynamic-import))
- `numericSeparator` ([proposal](https://github.com/samuelgoto/proposal-numeric-separator))
- `optionalChaining` ([proposal](https://github.com/tc39/proposal-optional-chaining))
- `importMeta` ([proposal](https://github.com/tc39/proposal-import-meta))

### FAQ
Expand Down
5 changes: 4 additions & 1 deletion ast/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -859,10 +859,11 @@ interface MemberExpression <: Expression, Pattern {
object: Expression | Super;
property: Expression;
computed: boolean;
optional: boolean | null;
}
```

A member expression. If `computed` is `true`, the node corresponds to a computed (`a[b]`) member expression and `property` is an `Expression`. If `computed` is `false`, the node corresponds to a static (`a.b`) member expression and `property` is an `Identifier`.
A member expression. If `computed` is `true`, the node corresponds to a computed (`a[b]`) member expression and `property` is an `Expression`. If `computed` is `false`, the node corresponds to a static (`a.b`) member expression and `property` is an `Identifier`. The `optional` flags indicates that the member expression can be called even if the object is null or undefined. If this is the object value (null/undefined) should be returned.

### BindExpression

Expand Down Expand Up @@ -896,6 +897,7 @@ interface CallExpression <: Expression {
type: "CallExpression";
callee: Expression | Super | Import;
arguments: [ Expression | SpreadElement ];
optional: boolean | null;
}
```

Expand All @@ -906,6 +908,7 @@ A function or method call expression.
```js
interface NewExpression <: CallExpression {
type: "NewExpression";
optional: boolean | null;
}
```

Expand Down
42 changes: 42 additions & 0 deletions src/parser/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,44 @@ export default class ExpressionParser extends LValParser {
node.object = base;
node.callee = this.parseNoCallExpr();
return this.parseSubscripts(this.finishNode(node, "BindExpression"), startPos, startLoc, noCalls);

} else if (this.match(tt.questionDot)) {
if (!this.hasPlugin("optionalChaining")) {
this.raise(startPos, "You can only use optional-chaining when the 'optionalChaining' plugin is enabled.");
}

if (noCalls && this.lookahead().type == tt.parenL) {
return base;
}
this.next();

const node = this.startNodeAt(startPos, startLoc);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably just move node.optional = true; to here since it's in all the if conditionals?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to keep the property ordering consistent between optional and non-optional branches. See the #parseNew as well.

if (this.eat(tt.bracketL)) {
node.object = base;
node.property = this.parseExpression();
node.computed = true;
node.optional = true;
this.expect(tt.bracketR);
base = this.finishNode(node, "MemberExpression");
} else if (this.eat(tt.parenL)) {
const possibleAsync = this.state.potentialArrowAt === base.start &&
base.type === "Identifier" &&
base.name === "async" &&
!this.canInsertSemicolon();

node.callee = base;
node.arguments = this.parseCallExpressionArguments(tt.parenR, possibleAsync);
node.optional = true;

base = this.finishNode(node, "CallExpression");
} else {
node.object = base;
node.property = this.parseIdentifier(true);
node.computed = false;
node.optional = true;
base = this.finishNode(node, "MemberExpression");
}
} else if (this.eat(tt.dot)) {
const node = this.startNodeAt(startPos, startLoc);
node.object = base;
Expand Down Expand Up @@ -736,13 +774,17 @@ export default class ExpressionParser extends LValParser {
}

node.callee = this.parseNoCallExpr();
const optional = this.eat(tt.questionDot);

if (this.eat(tt.parenL)) {
node.arguments = this.parseExprList(tt.parenR);
this.toReferencedList(node.arguments);
} else {
node.arguments = [];
}
if (optional) {
node.optional = true;
}

return this.finishNode(node, "NewExpression");
}
Expand Down
16 changes: 14 additions & 2 deletions src/tokenizer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,18 @@ export default class Tokenizer extends LocationParser {
return this.finishOp(code === 61 ? tt.eq : tt.prefix, 1);
}

readToken_question() { // '?'
const next = this.input.charCodeAt(this.state.pos + 1);
const next2 = this.input.charCodeAt(this.state.pos + 2);
if (next === 46 && !(next2 >= 48 && next2 <= 57)) { // '.' not followed by a number
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are these magic numbers? could these be stored in named variables, so it's clear what "46" means?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are char codes. I'll add some contants to improve readability, thanks.

this.state.pos += 2;
return this.finishToken(tt.questionDot);
} else {
++this.state.pos;
return this.finishToken(tt.question);
}
}

getTokenFromCode(code: number): void {
switch (code) {

Expand Down Expand Up @@ -469,7 +481,7 @@ export default class Tokenizer extends LocationParser {
return this.finishToken(tt.colon);
}

case 63: ++this.state.pos; return this.finishToken(tt.question);
case 63: return this.readToken_question();
case 64: ++this.state.pos; return this.finishToken(tt.at);

case 96: // '`'
Expand Down Expand Up @@ -917,7 +929,7 @@ export default class Tokenizer extends LocationParser {
const type = this.state.type;
let update;

if (type.keyword && prevType === tt.dot) {
if (type.keyword && (prevType === tt.dot || prevType === tt.questionDot)) {
this.state.exprAllowed = false;
} else if (update = type.updateContext) {
update.call(this, prevType);
Expand Down
1 change: 1 addition & 0 deletions src/tokenizer/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const types: { [name: string]: TokenType } = {
doubleColon: new TokenType("::", { beforeExpr }),
dot: new TokenType("."),
question: new TokenType("?", { beforeExpr }),
questionDot: new TokenType("?."),
arrow: new TokenType("=>", { beforeExpr }),
template: new TokenType("template"),
ellipsis: new TokenType("...", { beforeExpr }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
new C?.()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't have an expected?

Copy link
Member Author

@xtuc xtuc May 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't parse yet, need to be implemented (i'm still WIP)


new C?.(a, b)

new B?.C?.()

new B?.C?.(a, b)

new B?.C
Loading