JSTQL-JS-Transform/output_testing/48transform-test-gate-pragma.js

332 lines
9 KiB
JavaScript
Raw Normal View History

'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;