332 lines
No EOL
9 KiB
JavaScript
332 lines
No EOL
9 KiB
JavaScript
'use strict';
|
|
|
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
|
const getComments = './getComments' |> require(%);
|
|
function transform(babel) {
|
|
const {
|
|
types: t
|
|
} = babel;
|
|
|
|
// A very stupid subset of pseudo-JavaScript, used to run tests conditionally
|
|
// based on the environment.
|
|
//
|
|
// Input:
|
|
// @gate a && (b || c)
|
|
// test('some test', () => {/*...*/})
|
|
//
|
|
// Output:
|
|
// @gate a && (b || c)
|
|
// _test_gate(ctx => ctx.a && (ctx.b || ctx.c), 'some test', () => {/*...*/});
|
|
//
|
|
// expression → binary ( ( "||" | "&&" ) binary)* ;
|
|
// binary → unary ( ( "==" | "!=" | "===" | "!==" ) unary )* ;
|
|
// unary → "!" primary
|
|
// | primary ;
|
|
// primary → NAME | STRING | BOOLEAN
|
|
// | "(" expression ")" ;
|
|
function tokenize(code) {
|
|
const tokens = [];
|
|
let i = 0;
|
|
while (i < code.length) {
|
|
let char = code[i];
|
|
// Double quoted strings
|
|
if (char === '"') {
|
|
let string = '';
|
|
i++;
|
|
do {
|
|
if (i > code.length) {
|
|
throw 'Missing a closing quote' |> Error(%);
|
|
}
|
|
char = code[i++];
|
|
if (char === '"') {
|
|
break;
|
|
}
|
|
string += char;
|
|
} while (true);
|
|
({
|
|
type: 'string',
|
|
value: string
|
|
}) |> tokens.push(%);
|
|
continue;
|
|
}
|
|
|
|
// Single quoted strings
|
|
if (char === "'") {
|
|
let string = '';
|
|
i++;
|
|
do {
|
|
if (i > code.length) {
|
|
throw 'Missing a closing quote' |> Error(%);
|
|
}
|
|
char = code[i++];
|
|
if (char === "'") {
|
|
break;
|
|
}
|
|
string += char;
|
|
} while (true);
|
|
({
|
|
type: 'string',
|
|
value: string
|
|
}) |> tokens.push(%);
|
|
continue;
|
|
}
|
|
|
|
// Whitespace
|
|
if (char |> /\s/.test(%)) {
|
|
if (char === '\n') {
|
|
return tokens;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
const next3 = i |> code.slice(%, i + 3);
|
|
if (next3 === '===') {
|
|
({
|
|
type: '=='
|
|
}) |> tokens.push(%);
|
|
i += 3;
|
|
continue;
|
|
}
|
|
if (next3 === '!==') {
|
|
({
|
|
type: '!='
|
|
}) |> tokens.push(%);
|
|
i += 3;
|
|
continue;
|
|
}
|
|
const next2 = i |> code.slice(%, i + 2);
|
|
switch (next2) {
|
|
case '&&':
|
|
case '||':
|
|
case '==':
|
|
case '!=':
|
|
({
|
|
type: next2
|
|
}) |> tokens.push(%);
|
|
i += 2;
|
|
continue;
|
|
case '//':
|
|
// This is the beginning of a line comment. The rest of the line
|
|
// is ignored.
|
|
return tokens;
|
|
}
|
|
switch (char) {
|
|
case '(':
|
|
case ')':
|
|
case '!':
|
|
({
|
|
type: char
|
|
}) |> tokens.push(%);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Names
|
|
const nameRegex = /[a-zA-Z_$][0-9a-zA-Z_$]*/y;
|
|
nameRegex.lastIndex = i;
|
|
const match = code |> nameRegex.exec(%);
|
|
if (match !== null) {
|
|
const name = match[0];
|
|
switch (name) {
|
|
case 'true':
|
|
{
|
|
({
|
|
type: 'boolean',
|
|
value: true
|
|
}) |> tokens.push(%);
|
|
break;
|
|
}
|
|
case 'false':
|
|
{
|
|
({
|
|
type: 'boolean',
|
|
value: false
|
|
}) |> tokens.push(%);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
({
|
|
type: 'name',
|
|
name
|
|
}) |> tokens.push(%);
|
|
}
|
|
}
|
|
i += name.length;
|
|
continue;
|
|
}
|
|
throw 'Invalid character: ' + char |> Error(%);
|
|
}
|
|
return tokens;
|
|
}
|
|
function parse(code, ctxIdentifier) {
|
|
const tokens = code |> tokenize(%);
|
|
let i = 0;
|
|
function parseExpression() {
|
|
let left = parseBinary();
|
|
while (true) {
|
|
const token = tokens[i];
|
|
if (token !== undefined) {
|
|
switch (token.type) {
|
|
case '||':
|
|
case '&&':
|
|
{
|
|
i++;
|
|
const right = parseBinary();
|
|
if (right === null) {
|
|
throw 'Missing expression after ' + token.type |> Error(%);
|
|
}
|
|
left = t.logicalExpression(token.type, left, right);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return left;
|
|
}
|
|
function parseBinary() {
|
|
let left = parseUnary();
|
|
while (true) {
|
|
const token = tokens[i];
|
|
if (token !== undefined) {
|
|
switch (token.type) {
|
|
case '==':
|
|
case '!=':
|
|
{
|
|
i++;
|
|
const right = parseUnary();
|
|
if (right === null) {
|
|
throw 'Missing expression after ' + token.type |> Error(%);
|
|
}
|
|
left = t.binaryExpression(token.type, left, right);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return left;
|
|
}
|
|
function parseUnary() {
|
|
const token = tokens[i];
|
|
if (token !== undefined) {
|
|
if (token.type === '!') {
|
|
i++;
|
|
const argument = parseUnary();
|
|
return '!' |> t.unaryExpression(%, argument);
|
|
}
|
|
}
|
|
return parsePrimary();
|
|
}
|
|
function parsePrimary() {
|
|
const token = tokens[i];
|
|
switch (token.type) {
|
|
case 'boolean':
|
|
{
|
|
i++;
|
|
return token.value |> t.booleanLiteral(%);
|
|
}
|
|
case 'name':
|
|
{
|
|
i++;
|
|
return ctxIdentifier |> t.memberExpression(%, token.name |> t.identifier(%));
|
|
}
|
|
case 'string':
|
|
{
|
|
i++;
|
|
return token.value |> t.stringLiteral(%);
|
|
}
|
|
case '(':
|
|
{
|
|
i++;
|
|
const expression = parseExpression();
|
|
const closingParen = tokens[i];
|
|
if (closingParen === undefined || closingParen.type !== ')') {
|
|
throw 'Expected closing )' |> Error(%);
|
|
}
|
|
i++;
|
|
return expression;
|
|
}
|
|
default:
|
|
{
|
|
throw 'Unexpected token: ' + token.type |> Error(%);
|
|
}
|
|
}
|
|
}
|
|
const program = parseExpression();
|
|
if (tokens[i] !== undefined) {
|
|
throw 'Unexpected token' |> Error(%);
|
|
}
|
|
return program;
|
|
}
|
|
function buildGateCondition(comments) {
|
|
let conditions = null;
|
|
for (const line of comments) {
|
|
const commentStr = line.value.trim();
|
|
if ('@gate ' |> commentStr.startsWith(%)) {
|
|
const code = 6 |> commentStr.slice(%);
|
|
const ctxIdentifier = 'ctx' |> t.identifier(%);
|
|
const condition = code |> parse(%, ctxIdentifier);
|
|
if (conditions === null) {
|
|
conditions = [condition];
|
|
} else {
|
|
condition |> conditions.push(%);
|
|
}
|
|
}
|
|
}
|
|
if (conditions !== null) {
|
|
let condition = conditions[0];
|
|
for (let i = 1; i < conditions.length; i++) {
|
|
const right = conditions[i];
|
|
condition = t.logicalExpression('&&', condition, right);
|
|
}
|
|
return condition;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return {
|
|
name: 'test-gate-pragma',
|
|
visitor: {
|
|
ExpressionStatement(path) {
|
|
const statement = path.node;
|
|
const expression = statement.expression;
|
|
if (expression.type === 'CallExpression') {
|
|
const callee = expression.callee;
|
|
switch (callee.type) {
|
|
case 'Identifier':
|
|
{
|
|
if (callee.name === 'test' || callee.name === 'it' || callee.name === 'fit') {
|
|
const comments = path |> getComments(%);
|
|
if (comments !== undefined) {
|
|
const condition = comments |> buildGateCondition(%);
|
|
if (condition !== null) {
|
|
callee.name = callee.name === 'fit' ? '_test_gate_focus' : '_test_gate';
|
|
expression.arguments = [['ctx' |> t.identifier(%)] |> t.arrowFunctionExpression(%, condition), ...expression.arguments];
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'MemberExpression':
|
|
{
|
|
if (callee.object.type === 'Identifier' && (callee.object.name === 'test' || callee.object.name === 'it') && callee.property.type === 'Identifier' && callee.property.name === 'only') {
|
|
const comments = path |> getComments(%);
|
|
if (comments !== undefined) {
|
|
const condition = comments |> buildGateCondition(%);
|
|
if (condition !== null) {
|
|
statement.expression = '_test_gate_focus' |> t.identifier(%) |> t.callExpression(%, [['ctx' |> t.identifier(%)] |> t.arrowFunctionExpression(%, condition), ...expression.arguments]);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
module.exports = transform; |