Skip to content

Commit

Permalink
refactor: restructure ChainedComparisonOp to make chained operands …
Browse files Browse the repository at this point in the history
…explicit (#145)

Refs decaffeinate/decaffeinate#783

BREAKING CHANGE: Rather than relying on a visitor to infer which nested binary operations are part of the chain, this flattens the structure to enumerate the operands and operators in lists.
  • Loading branch information
eventualbuddha authored Feb 5, 2017
1 parent a9a8c5a commit 2f21b1e
Show file tree
Hide file tree
Showing 16 changed files with 673 additions and 229 deletions.
72 changes: 67 additions & 5 deletions src/mappers/mapOp.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
import { Op as CoffeeOp, Return as CoffeeReturn } from 'decaffeinate-coffeescript/lib/coffee-script/nodes';
import { inspect } from 'util';
import { MultiplyOp, Node, Op, SubtractOp, UnaryNegateOp, Yield, YieldFrom, YieldReturn } from '../nodes';
import {
BinaryOp, ChainedComparisonOp, EQOp, MultiplyOp, Node, Op, OperatorInfo, SubtractOp, UnaryNegateOp, Yield, YieldFrom, YieldReturn
} from '../nodes';
import getOperatorInfoInRange from '../util/getOperatorInfoInRange';
import isChainedComparison from '../util/isChainedComparison';
import ParseContext from '../util/ParseContext';
import unwindChainedComparison from '../util/unwindChainedComparison';
import mapAny from './mapAny';
import { UnsupportedNodeError } from './mapAnyWithFallback';
import mapBase from './mapBase';

export default function mapOp(context: ParseContext, node: CoffeeOp): Node {
if (isChainedComparison(node)) {
return mapChainedComparisonOp(context, node);
} else {
return mapOpWithoutChainedComparison(context, node);
}
}

function mapChainedComparisonOp(context: ParseContext, node: CoffeeOp) {
let { line, column, start, end, raw, virtual } = mapBase(context, node);
let coffeeOperands = unwindChainedComparison(node);
let operands = coffeeOperands.map(operand => mapAny(context, operand));
let operators: Array<OperatorInfo> = [];

for (let i = 0; i < operands.length - 1; i++) {
let left = operands[i];
let right = operands[i + 1];

operators.push(getOperatorInfoInRange(context, left.end, right.start));
}

return new ChainedComparisonOp(
line,
column,
start,
end,
raw,
virtual,
operands,
operators
);
}

function mapOpWithoutChainedComparison(context: ParseContext, node: CoffeeOp): Node {
switch (node.operator) {
case '===':
return mapEqualityOp(context, node);

case '-':
return mapSubtractOp(context, node);

Expand All @@ -24,6 +65,10 @@ export default function mapOp(context: ParseContext, node: CoffeeOp): Node {
throw new UnsupportedNodeError(node);
}

function mapEqualityOp(context: ParseContext, node: CoffeeOp) {
return mapBinaryOp(context, node, EQOp);
}

function mapSubtractOp(context: ParseContext, node: CoffeeOp): Op {
let { line, column, start, end, raw, virtual } = mapBase(context, node);

Expand All @@ -41,21 +86,38 @@ function mapSubtractOp(context: ParseContext, node: CoffeeOp): Op {
}
}

function mapMultiplyOp(context: ParseContext, node: CoffeeOp): Op {
interface IBinaryOp {
new(
line: number,
column: number,
start: number,
end: number,
raw: string,
virtual: boolean,
left: Node,
right: Node
): BinaryOp;
}

function mapBinaryOp<T extends IBinaryOp>(context: ParseContext, node: CoffeeOp, Op: T): BinaryOp {
let { line, column, start, end, raw, virtual } = mapBase(context, node);

if (!node.second) {
throw new Error(`unexpected '*' operator with only one operand: ${inspect(node)}`);
throw new Error(`unexpected '${node.operator}' operator with only one operand: ${inspect(node)}`);
}

return new MultiplyOp(
return new Op(
line, column, start, end, raw, virtual,
mapAny(context, node.first),
mapAny(context, node.second)
);
}

function mapYieldOp(context: ParseContext, node: CoffeeOp): Node {
function mapMultiplyOp(context: ParseContext, node: CoffeeOp): MultiplyOp {
return mapBinaryOp(context, node, MultiplyOp);
}

function mapYieldOp(context: ParseContext, node: CoffeeOp): YieldReturn | Yield {
let { line, column, start, end, raw, virtual } = mapBase(context, node);

if (node.first instanceof CoffeeReturn) {
Expand Down
52 changes: 49 additions & 3 deletions src/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SourceType } from 'coffee-lex';
import SourceToken from 'coffee-lex/dist/SourceToken';
import { LocationData } from 'decaffeinate-coffeescript/lib/coffee-script/nodes';
import { inspect } from 'util';
import ParseContext from './util/ParseContext';
Expand Down Expand Up @@ -840,7 +841,7 @@ export class Range extends Node {
}
}

export class BinaryOp extends Node {
export abstract class BinaryOp extends Node {
readonly left: Node;
readonly right: Node;

Expand All @@ -861,7 +862,7 @@ export class BinaryOp extends Node {
}
}

export class UnaryOp extends Node {
export abstract class UnaryOp extends Node {
readonly expression: Node;

constructor(
Expand All @@ -879,7 +880,52 @@ export class UnaryOp extends Node {
}
}

export type Op = UnaryOp | BinaryOp;
export class ChainedComparisonOp extends Node {
readonly operands: Array<Node>;
readonly operators: Array<OperatorInfo>;

constructor(
line: number,
column: number,
start: number,
end: number,
raw: string,
virtual: boolean,
operands: Array<Node>,
operators: Array<OperatorInfo>
) {
super('ChainedComparisonOp', line, column, start, end, raw, virtual);
this.operands = operands;
this.operators = operators;
}
}

export class OperatorInfo {
readonly operator: string;
readonly token: SourceToken;

constructor(operator: string, token: SourceToken) {
this.operator = operator;
this.token = token;
}
}

export type Op = UnaryOp | BinaryOp | ChainedComparisonOp;

export class EQOp extends BinaryOp {
constructor(
line: number,
column: number,
start: number,
end: number,
raw: string,
virtual: boolean,
left: Node,
right: Node
) {
super('EQOp', line, column, start, end, raw, virtual, left, right);
}
}

export class SubtractOp extends BinaryOp {
constructor(
Expand Down
17 changes: 15 additions & 2 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as CoffeeScript from 'decaffeinate-coffeescript';
import ParseContext from './util/ParseContext';
import expandLocationLeftThrough from './util/expandLocationLeftThrough';
import isChainedComparison from './util/isChainedComparison';
import unwindChainedComparison from './util/unwindChainedComparison';
import getOperatorInfoInRange from './util/getOperatorInfoInRange';
import isHeregexTemplateNode from './util/isHeregexTemplateNode';
import isImplicitPlusOp from './util/isImplicitPlusOp';
import isInterpolatedString from './util/isInterpolatedString';
Expand Down Expand Up @@ -417,9 +419,20 @@ function convert(context: ParseContext, map: (context: ParseContext, node: Base,
if (isImplicitPlusOp(op, context) && isInterpolatedString(node, ancestors, context)) {
return createTemplateLiteral(op, 'String');
}
if (isChainedComparison(node) && !isChainedComparison(ancestors[ancestors.length - 1])) {
if (isChainedComparison(node)) {
let operands = unwindChainedComparison(node).map(convertChild);
let operators = [];

for (let i = 0; i < operands.length - 1; i++) {
let left = operands[i];
let right = operands[i + 1];

operators.push(getOperatorInfoInRange(context, left.range[1], right.range[0]));
}

return makeNode(context, 'ChainedComparisonOp', node.locationData, {
expression: op
operands,
operators
});
}
return op;
Expand Down
29 changes: 29 additions & 0 deletions src/util/getOperatorInfoInRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import SourceType from 'coffee-lex/dist/SourceType';
import { OperatorInfo } from '../nodes';
import ParseContext from './ParseContext';

export default function getOperatorInfoInRange(context: ParseContext, start: number, end: number): OperatorInfo {
let startIndex = context.sourceTokens.indexOfTokenNearSourceIndex(start);
let endIndex = context.sourceTokens.indexOfTokenNearSourceIndex(end);

if (!startIndex || !endIndex) {
throw new Error(`cannot find token indexes of range bounds: [${start}, ${end}]`);
}

let operatorIndex = context.sourceTokens.indexOfTokenMatchingPredicate(
token => token.type !== SourceType.LPAREN && token.type !== SourceType.RPAREN,
startIndex.next(),
endIndex
);

let operatorToken = operatorIndex && context.sourceTokens.tokenAtIndex(operatorIndex);

if (!operatorToken) {
throw new Error(`cannot find operator token in range: [${start}, ${end}]`);
}

return new OperatorInfo(
context.source.slice(operatorToken.start, operatorToken.end),
operatorToken
);
}
25 changes: 25 additions & 0 deletions src/util/unwindChainedComparison.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Base, Op } from 'decaffeinate-coffeescript/lib/coffee-script/nodes';
import { inspect } from 'util';

export default function unwindChainedComparison(node: Op): Array<Base> {
let operands: Array<Base> = [];

for (let link: Base = node;;) {
if (link instanceof Op) {
let { first, second } = link;

if (!second) {
throw new Error(`unexpected unary operator inside chained comparison: ${inspect(node)}`);
}

operands = [second, ...operands];

link = first;
} else {
operands = [link, ...operands];
break;
}
}

return operands;
}
72 changes: 36 additions & 36 deletions test/examples/chained-comparison-equals/output.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,47 +24,30 @@
0,
11
],
"expression": {
"type": "EQOp",
"line": 1,
"column": 1,
"range": [
0,
11
],
"left": {
"type": "EQOp",
"operands": [
{
"type": "Identifier",
"line": 1,
"column": 1,
"range": [
0,
1
],
"data": "a",
"raw": "a"
},
{
"type": "Identifier",
"line": 1,
"column": 6,
"range": [
5,
6
],
"left": {
"type": "Identifier",
"line": 1,
"column": 1,
"range": [
0,
1
],
"data": "a",
"raw": "a"
},
"right": {
"type": "Identifier",
"line": 1,
"column": 6,
"range": [
5,
6
],
"data": "b",
"raw": "b"
},
"raw": "a == b"
"data": "b",
"raw": "b"
},
"right": {
{
"type": "Identifier",
"line": 1,
"column": 11,
Expand All @@ -74,9 +57,26 @@
],
"data": "c",
"raw": "c"
}
],
"operators": [
{
"operator": "==",
"token": {
"type": 39,
"start": 2,
"end": 4
}
},
"raw": "a == b == c"
},
{
"operator": "==",
"token": {
"type": 39,
"start": 7,
"end": 9
}
}
],
"raw": "a == b == c"
}
],
Expand Down
Loading

0 comments on commit 2f21b1e

Please sign in to comment.