Skip to content

Commit

Permalink
feat(parser): add expression operators 'in', 'instanceof', 'typeof', …
Browse files Browse the repository at this point in the history
…'void'

tests(parser): cleanup / add unit tests
  • Loading branch information
fkleuver committed Jun 6, 2018
1 parent f76de45 commit e849661
Show file tree
Hide file tree
Showing 7 changed files with 837 additions and 895 deletions.
29 changes: 29 additions & 0 deletions src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ export class Binary extends Expression {
case '===': return left === right;
case '!=' : return left != right; // eslint-disable-line eqeqeq
case '!==': return left !== right;
case 'instanceof': return typeof right === 'function' && left instanceof right;
case 'in': return typeof right === 'object' && right !== null && left in right;
// no default
}

Expand Down Expand Up @@ -551,6 +553,33 @@ export class PrefixNot extends Expression {
}
}

export class PrefixUnary extends Expression {
constructor(operation, expression) {
super();

this.operation = operation;
this.expression = expression;
}

evaluate(scope, lookupFunctions) {
switch (this.operation) {
case 'typeof': return typeof this.expression.evaluate(scope, lookupFunctions);
case 'void': return void this.expression.evaluate(scope, lookupFunctions);
// no default
}

throw new Error(`Internal error [${this.operation}] not handled`);
}

accept(visitor) {
return visitor.visitPrefix(this);
}

connect(binding, scope) {
this.expression.connect(binding, scope);
}
}

export class LiteralPrimitive extends Expression {
constructor(value) {
super();
Expand Down
25 changes: 16 additions & 9 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
AccessThis, AccessScope, AccessMember, AccessKeyed,
CallScope, CallFunction, CallMember,
PrefixNot, BindingBehavior, Binary,
LiteralPrimitive, LiteralArray, LiteralObject, LiteralString
LiteralPrimitive, LiteralArray, LiteralObject, LiteralString, PrefixUnary
} from './ast';

export class Parser {
Expand Down Expand Up @@ -132,6 +132,11 @@ export class ParserImplementation {
case T$Bang:
this.nextToken();
return new PrefixNot('!', this.parseLeftHandSideExpression(0));
case T$TypeofKeyword:
case T$VoidKeyword:
const op = TokenValues[this.currentToken & T$TokenMask];
this.nextToken();
return new PrefixUnary(op, this.parseLeftHandSideExpression(0));
case T$ParentScope: // $parent
{
do {
Expand All @@ -155,8 +160,8 @@ export class ParserImplementation {
// falls through
case T$Identifier: // identifier
{
this.nextToken();
result = new AccessScope(this.tokenValue, context & C$Ancestor);
this.nextToken();
context = (context & C$ShorthandProp) | C$Scope;
break;
}
Expand Down Expand Up @@ -575,12 +580,13 @@ const T$MemberOrCallExpression = 1 << 24;
/** 'instanceof' */const T$InstanceOfKeyword = 33/*11*/ | 5 << T$PrecShift | T$BinaryOp | T$Keyword;
/** '+' */ const T$Plus = 34/*13*/ | 6 << T$PrecShift | T$BinaryOp | T$UnaryOp;
/** '-' */ const T$Minus = 35/*13*/ | 6 << T$PrecShift | T$BinaryOp | T$UnaryOp;
/** 'typeof' */ const T$TypeofKeyword = 36/*16*/ | 8 << T$PrecShift | T$BinaryOp | T$UnaryOp | T$Keyword;
/** '*' */ const T$Star = 37/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '%' */ const T$Percent = 38/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '/' */ const T$Slash = 39/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '=' */ const T$Eq = 40;
/** '!' */ const T$Bang = 41 | T$UnaryOp;
/** 'typeof' */ const T$TypeofKeyword = 36/*16*/ | T$UnaryOp | T$Keyword;
/** 'void' */ const T$VoidKeyword = 37/*16*/ | T$UnaryOp | T$Keyword;
/** '*' */ const T$Star = 38/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '%' */ const T$Percent = 39/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '/' */ const T$Slash = 40/*14*/ | 7 << T$PrecShift | T$BinaryOp;
/** '=' */ const T$Eq = 41;
/** '!' */ const T$Bang = 42 | T$UnaryOp;

const KeywordLookup = Object.create(null);
KeywordLookup.true = T$TrueKeyword;
Expand All @@ -592,6 +598,7 @@ KeywordLookup.$parent = T$ParentScope;
KeywordLookup.in = T$InKeyword;
KeywordLookup.instanceof = T$InstanceOfKeyword;
KeywordLookup.typeof = T$TypeofKeyword;
KeywordLookup.void = T$VoidKeyword;

/**
* Array for mapping tokens to token values. The indices of the values
Expand All @@ -606,7 +613,7 @@ const TokenValues = [
'(', '{', '.', '}', ')', ';', ',', '[', ']', ':', '?', '\'', '"',

'&', '|', '||', '&&', '^', '==', '!=', '===', '!==', '<', '>',
'<=', '>=', 'in', 'instanceof', '+', '-', 'typeof', '*', '%', '/', '=', '!'
'<=', '>=', 'in', 'instanceof', '+', '-', 'typeof', 'void', '*', '%', '/', '=', '!'
];

/**
Expand Down
12 changes: 11 additions & 1 deletion src/unparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,23 @@ if (typeof FEATURE_NO_UNPARSER === 'undefined') {

visitPrefix(prefix) {
this.write(`(${prefix.operation}`);
if (prefix.operation.charCodeAt(0) >= /*a*/97) {
// add a space after if it's a keyword unary operator
// note: the Bitwise NOT (~) has charCode 126, so if/when that operator is added, it should be excluded here
this.write(' ');
}
prefix.expression.accept(this);
this.write(')');
}

visitBinary(binary) {
binary.left.accept(this);
this.write(binary.operation);
if (binary.operation.charCodeAt(0) === /*i*/105) {
// add a space before and after if it's either 'in' or 'instanceof'
this.write(` ${binary.operation} `);
} else {
this.write(binary.operation);
}
binary.right.accept(this);
}

Expand Down
52 changes: 51 additions & 1 deletion test/ast/binary.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Binary, LiteralString, LiteralPrimitive} from '../../src/ast';
import {Binary, LiteralString, LiteralPrimitive, LiteralObject, AccessThis, AccessScope, AccessMember } from '../../src/ast';
import {createScopeForTest} from '../../src/scope';

describe('Binary', () => {
Expand Down Expand Up @@ -45,4 +45,54 @@ describe('Binary', () => {
scope = createScopeForTest({});
expect(expression.evaluate(scope, null)).toBe(2);
});

describe('performs \'in\'', () => {
const tests = [
{ expr: new Binary('in', new LiteralString('foo'), new LiteralObject(['foo'], [new LiteralPrimitive(null)])), expected: true },
{ expr: new Binary('in', new LiteralString('foo'), new LiteralObject(['bar'], [new LiteralPrimitive(null)])), expected: false },
{ expr: new Binary('in', new LiteralPrimitive(1), new LiteralObject(['1'], [new LiteralPrimitive(null)])), expected: true },
{ expr: new Binary('in', new LiteralString('1'), new LiteralObject(['1'], [new LiteralPrimitive(null)])), expected: true },
{ expr: new Binary('in', new LiteralString('foo'), new LiteralPrimitive(null)), expected: false },
{ expr: new Binary('in', new LiteralString('foo'), new LiteralPrimitive(undefined)), expected: false },
{ expr: new Binary('in', new LiteralString('foo'), new LiteralPrimitive(true)), expected: false },
{ expr: new Binary('in', new LiteralString('foo'), new AccessThis(0)), expected: true },
{ expr: new Binary('in', new LiteralString('bar'), new AccessThis(0)), expected: true },
{ expr: new Binary('in', new LiteralString('foo'), new AccessThis(1)), expected: false },
{ expr: new Binary('in', new LiteralString('bar'), new AccessThis(1)), expected: false },
{ expr: new Binary('in', new LiteralString('foo'), new AccessScope('foo', 0)), expected: false },
{ expr: new Binary('in', new LiteralString('bar'), new AccessScope('bar', 0)), expected: false },
{ expr: new Binary('in', new LiteralString('bar'), new AccessScope('foo', 0)), expected: true }
];
let scope = createScopeForTest({foo: {bar: null}, bar: null});

for (const { expr, expected } of tests) {
it(expr.toString(), () => {
expect(expr.evaluate(scope, null)).toBe(expected);
});
}
});

describe('performs \'instanceof\'', () => {
class Foo {}
class Bar extends Foo {}
const tests = [
{ expr: new Binary('instanceof', new AccessScope('foo', 0), new AccessMember(new AccessScope('foo', 0), 'constructor')), expected: true },
{ expr: new Binary('instanceof', new AccessScope('foo', 0), new AccessMember(new AccessScope('bar', 0), 'constructor')), expected: false },
{ expr: new Binary('instanceof', new AccessScope('bar', 0), new AccessMember(new AccessScope('bar', 0), 'constructor')), expected: true },
{ expr: new Binary('instanceof', new AccessScope('bar', 0), new AccessMember(new AccessScope('foo', 0), 'constructor')), expected: true },
{ expr: new Binary('instanceof', new LiteralString('foo'), new AccessMember(new AccessScope('foo', 0), 'constructor')), expected: false },
{ expr: new Binary('instanceof', new AccessScope('foo', 0), new AccessScope('foo', 0)), expected: false },
{ expr: new Binary('instanceof', new AccessScope('foo', 0), new LiteralPrimitive(null)), expected: false },
{ expr: new Binary('instanceof', new AccessScope('foo', 0), new LiteralPrimitive(undefined)), expected: false },
{ expr: new Binary('instanceof', new LiteralPrimitive(null), new AccessScope('foo', 0)), expected: false },
{ expr: new Binary('instanceof', new LiteralPrimitive(undefined), new AccessScope('foo', 0)), expected: false }
];
let scope = createScopeForTest({foo: new Foo(), bar: new Bar()});

for (const { expr, expected } of tests) {
it(expr.toString(), () => {
expect(expr.evaluate(scope, null)).toBe(expected);
});
}
});
});
59 changes: 59 additions & 0 deletions test/ast/prefix-unary.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { PrefixUnary, LiteralString, LiteralPrimitive, LiteralObject, AccessThis, AccessScope, AccessMember, LiteralArray, CallFunction, CallScope } from '../../src/ast';
import { createScopeForTest } from '../../src/scope';

describe('PrefixUnary', () => {
describe('performs \'typeof\'', () => {
const tests = [
{ expr: new PrefixUnary('typeof', new LiteralString('foo')), expected: 'string' },
{ expr: new PrefixUnary('typeof', new LiteralPrimitive(1)), expected: 'number' },
{ expr: new PrefixUnary('typeof', new LiteralPrimitive(null)), expected: 'object' },
{ expr: new PrefixUnary('typeof', new LiteralPrimitive(undefined)), expected: 'undefined' },
{ expr: new PrefixUnary('typeof', new LiteralPrimitive(true)), expected: 'boolean' },
{ expr: new PrefixUnary('typeof', new LiteralPrimitive(false)), expected: 'boolean' },
{ expr: new PrefixUnary('typeof', new LiteralArray([])), expected: 'object' },
{ expr: new PrefixUnary('typeof', new LiteralObject([], [])), expected: 'object' },
{ expr: new PrefixUnary('typeof', new AccessThis(0)), expected: 'object' },
{ expr: new PrefixUnary('typeof', new AccessThis(1)), expected: 'undefined' },
{ expr: new PrefixUnary('typeof', new AccessScope('foo', 0)), expected: 'undefined' }
];
let scope = createScopeForTest({});

for (const { expr, expected } of tests) {
it(expr.toString(), () => {
expect(expr.evaluate(scope, null)).toBe(expected);
});
}
});

describe('performs \'void\'', () => {
const tests = [
{ expr: new PrefixUnary('void', new LiteralString('foo')) },
{ expr: new PrefixUnary('void', new LiteralPrimitive(1)) },
{ expr: new PrefixUnary('void', new LiteralPrimitive(null)) },
{ expr: new PrefixUnary('void', new LiteralPrimitive(undefined)) },
{ expr: new PrefixUnary('void', new LiteralPrimitive(true)) },
{ expr: new PrefixUnary('void', new LiteralPrimitive(false)) },
{ expr: new PrefixUnary('void', new LiteralArray([])) },
{ expr: new PrefixUnary('void', new LiteralObject([], [])) },
{ expr: new PrefixUnary('void', new AccessThis(0)) },
{ expr: new PrefixUnary('void', new AccessThis(1)) },
{ expr: new PrefixUnary('void', new AccessScope('foo', 0)) }
];
let scope = createScopeForTest({});

for (const { expr } of tests) {
it(expr.toString(), () => {
expect(expr.evaluate(scope, null)).toBe(undefined);
});
}

it('void foo()', () => {
let fooCalled = false;
const foo = () => fooCalled = true;
scope = createScopeForTest({foo});
const expr = new PrefixUnary('void', new CallScope('foo', [], 0));
expect(expr.evaluate(scope, null)).toBe(undefined);
expect(fooCalled).toBe(true);
});
});
});
Loading

0 comments on commit e849661

Please sign in to comment.