Chained comparison AST (#5225)

* chained comparison AST

* Explicit return

Co-Authored-By: Geoffrey Booth <GeoffreyBooth@users.noreply.github.com>
This commit is contained in:
Julian Rosse
2019-07-24 10:11:42 -04:00
committed by Geoffrey Booth
parent 0174ee082c
commit c6fdde8834
4 changed files with 173 additions and 9 deletions

View File

@@ -6459,13 +6459,17 @@
return (ref1 = this.operator) === '<' || ref1 === '>' || ref1 === '>=' || ref1 === '<=' || ref1 === '===' || ref1 === '!==';
}
isChain() {
return this.isChainable() && this.first.isChainable();
}
invert() {
var allInvertable, curr, fst, op, ref1;
if (this.isInOperator()) {
this.invertOperator = '!';
return this;
}
if (this.isChainable() && this.first.isChainable()) {
if (this.isChain()) {
allInvertable = true;
curr = this;
while (curr && curr.operator) {
@@ -6538,7 +6542,7 @@
if (this.operator === 'do') {
return Op.prototype.generateDo(this.first).compileNode(o);
}
isChain = this.isChainable() && this.first.isChainable();
isChain = this.isChain();
if (!isChain) {
// In chains, there's no need to wrap bare obj literals in parens,
// as the chained expression is wrapped.
@@ -6697,6 +6701,9 @@
if (this.isYield()) {
return 'YieldExpression';
}
if (this.isChain()) {
return 'ChainedComparison';
}
switch (this.operator) {
case '||':
case '&&':
@@ -6714,10 +6721,46 @@
}
}
operatorAst() {
return `${this.invertOperator ? `${this.invertOperator} ` : ''}${this.originalOperator}`;
}
chainAstProperties(o) {
var currentOp, operand, operands, operators;
operators = [this.operatorAst()];
operands = [this.second];
currentOp = this.first;
while (true) {
operators.unshift(currentOp.operatorAst());
operands.unshift(currentOp.second);
currentOp = currentOp.first;
if (!currentOp.isChainable()) {
operands.unshift(currentOp);
break;
}
}
return {
operators,
operands: (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = operands.length; j < len1; j++) {
operand = operands[j];
results.push(operand.ast(o, LEVEL_OP));
}
return results;
})()
};
}
astProperties(o) {
var argument, firstAst, ref1, secondAst;
var argument, firstAst, operatorAst, ref1, secondAst;
if (this.isChain()) {
return this.chainAstProperties(o);
}
firstAst = this.first.ast(o, LEVEL_OP);
secondAst = (ref1 = this.second) != null ? ref1.ast(o, LEVEL_OP) : void 0;
operatorAst = this.operatorAst();
switch (false) {
case !this.isUnary():
argument = this.isYield() && this.first.unwrap().value === '' ? null : firstAst;
@@ -6732,14 +6775,14 @@
}
return {
argument,
operator: this.originalOperator,
operator: operatorAst,
prefix: !this.flip
};
default:
return {
left: firstAst,
right: secondAst,
operator: `${this.invertOperator ? `${this.invertOperator} ` : ''}${this.originalOperator}`
operator: operatorAst
};
}
}

View File

@@ -4342,11 +4342,14 @@ exports.Op = class Op extends Base
isChainable: ->
@operator in ['<', '>', '>=', '<=', '===', '!==']
isChain: ->
@isChainable() and @first.isChainable()
invert: ->
if @isInOperator()
@invertOperator = '!'
return @
if @isChainable() and @first.isChainable()
if @isChain()
allInvertable = yes
curr = this
while curr and curr.operator
@@ -4402,7 +4405,7 @@ exports.Op = class Op extends Base
@invertOperator = null
return @invert().compileNode(o)
return Op::generateDo(@first).compileNode o if @operator is 'do'
isChain = @isChainable() and @first.isChainable()
isChain = @isChain()
# In chains, there's no need to wrap bare obj literals in parens,
# as the chained expression is wrapped.
@first.front = @front unless isChain
@@ -4502,6 +4505,7 @@ exports.Op = class Op extends Base
astType: ->
return 'AwaitExpression' if @isAwait()
return 'YieldExpression' if @isYield()
return 'ChainedComparison' if @isChain()
switch @operator
when '||', '&&', '?' then 'LogicalExpression'
when '++', '--' then 'UpdateExpression'
@@ -4509,9 +4513,31 @@ exports.Op = class Op extends Base
if @isUnary() then 'UnaryExpression'
else 'BinaryExpression'
operatorAst: ->
"#{if @invertOperator then "#{@invertOperator} " else ''}#{@originalOperator}"
chainAstProperties: (o) ->
operators = [@operatorAst()]
operands = [@second]
currentOp = @first
loop
operators.unshift currentOp.operatorAst()
operands.unshift currentOp.second
currentOp = currentOp.first
unless currentOp.isChainable()
operands.unshift currentOp
break
return {
operators
operands: (operand.ast(o, LEVEL_OP) for operand in operands)
}
astProperties: (o) ->
return @chainAstProperties(o) if @isChain()
firstAst = @first.ast o, LEVEL_OP
secondAst = @second?.ast o, LEVEL_OP
operatorAst = @operatorAst()
switch
when @isUnary()
argument =
@@ -4526,14 +4552,14 @@ exports.Op = class Op extends Base
} if @isYield()
return {
argument
operator: @originalOperator
operator: operatorAst
prefix: !@flip
}
else
return
left: firstAst
right: secondAst
operator: "#{if @invertOperator then "#{@invertOperator} " else ''}#{@originalOperator}"
operator: operatorAst
#### In
exports.In = class In extends Base

View File

@@ -3829,3 +3829,48 @@ test "AST as expected for comments", ->
type: 'CommentBlock'
value: '\n # multiline\n # indented here comment\n '
]
test "AST as expected for chained comparisons", ->
testExpression '''
a < b < c
''',
type: 'ChainedComparison'
operands: [
ID 'a'
ID 'b'
ID 'c'
]
operators: [
'<'
'<'
]
testExpression '''
a isnt b is c isnt d
''',
type: 'ChainedComparison'
operands: [
ID 'a'
ID 'b'
ID 'c'
ID 'd'
]
operators: [
'isnt'
'is'
'isnt'
]
testExpression '''
a >= b < c
''',
type: 'ChainedComparison'
operands: [
ID 'a'
ID 'b'
ID 'c'
]
operators: [
'>='
'<'
]

View File

@@ -7854,3 +7854,53 @@ test "AST as expected for comments", ->
line: 5
column: 5
]
test "AST location data as expected for chained comparisons", ->
testAstLocationData '''
a >= b < c
''',
type: 'ChainedComparison'
operands: [
start: 0
end: 1
range: [0, 1]
loc:
start:
line: 1
column: 0
end:
line: 1
column: 1
,
start: 5
end: 6
range: [5, 6]
loc:
start:
line: 1
column: 5
end:
line: 1
column: 6
,
start: 9
end: 10
range: [9, 10]
loc:
start:
line: 1
column: 9
end:
line: 1
column: 10
]
start: 0
end: 10
range: [0, 10]
loc:
start:
line: 1
column: 0
end:
line: 1
column: 10