337 lines
8.8 KiB
JavaScript
337 lines
8.8 KiB
JavaScript
'use strict';
|
|
|
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
|
|
|
const getComments = require('./getComments');
|
|
|
|
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 Error('Missing a closing quote');
|
|
}
|
|
char = code[i++];
|
|
if (char === '"') {
|
|
break;
|
|
}
|
|
string += char;
|
|
} while (true);
|
|
tokens.push({type: 'string', value: string});
|
|
continue;
|
|
}
|
|
|
|
// Single quoted strings
|
|
if (char === "'") {
|
|
let string = '';
|
|
i++;
|
|
do {
|
|
if (i > code.length) {
|
|
throw Error('Missing a closing quote');
|
|
}
|
|
char = code[i++];
|
|
if (char === "'") {
|
|
break;
|
|
}
|
|
string += char;
|
|
} while (true);
|
|
tokens.push({type: 'string', value: string});
|
|
continue;
|
|
}
|
|
|
|
// Whitespace
|
|
if (/\s/.test(char)) {
|
|
if (char === '\n') {
|
|
return tokens;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const next3 = code.slice(i, i + 3);
|
|
if (next3 === '===') {
|
|
tokens.push({type: '=='});
|
|
i += 3;
|
|
continue;
|
|
}
|
|
if (next3 === '!==') {
|
|
tokens.push({type: '!='});
|
|
i += 3;
|
|
continue;
|
|
}
|
|
|
|
const next2 = code.slice(i, i + 2);
|
|
switch (next2) {
|
|
case '&&':
|
|
case '||':
|
|
case '==':
|
|
case '!=':
|
|
tokens.push({type: next2});
|
|
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 '!':
|
|
tokens.push({type: char});
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Names
|
|
const nameRegex = /[a-zA-Z_$][0-9a-zA-Z_$]*/y;
|
|
nameRegex.lastIndex = i;
|
|
const match = nameRegex.exec(code);
|
|
if (match !== null) {
|
|
const name = match[0];
|
|
switch (name) {
|
|
case 'true': {
|
|
tokens.push({type: 'boolean', value: true});
|
|
break;
|
|
}
|
|
case 'false': {
|
|
tokens.push({type: 'boolean', value: false});
|
|
break;
|
|
}
|
|
default: {
|
|
tokens.push({type: 'name', name});
|
|
}
|
|
}
|
|
i += name.length;
|
|
continue;
|
|
}
|
|
|
|
throw Error('Invalid character: ' + char);
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
function parse(code, ctxIdentifier) {
|
|
const tokens = tokenize(code);
|
|
|
|
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 Error('Missing expression after ' + token.type);
|
|
}
|
|
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 Error('Missing expression after ' + token.type);
|
|
}
|
|
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 t.booleanLiteral(token.value);
|
|
}
|
|
case 'name': {
|
|
i++;
|
|
return t.memberExpression(ctxIdentifier, t.identifier(token.name));
|
|
}
|
|
case 'string': {
|
|
i++;
|
|
return t.stringLiteral(token.value);
|
|
}
|
|
case '(': {
|
|
i++;
|
|
const expression = parseExpression();
|
|
const closingParen = tokens[i];
|
|
if (closingParen === undefined || closingParen.type !== ')') {
|
|
throw Error('Expected closing )');
|
|
}
|
|
i++;
|
|
return expression;
|
|
}
|
|
default: {
|
|
throw Error('Unexpected token: ' + token.type);
|
|
}
|
|
}
|
|
}
|
|
|
|
const program = parseExpression();
|
|
if (tokens[i] !== undefined) {
|
|
throw Error('Unexpected token');
|
|
}
|
|
return program;
|
|
}
|
|
|
|
function buildGateCondition(comments) {
|
|
let conditions = null;
|
|
for (const line of comments) {
|
|
const commentStr = line.value.trim();
|
|
if (commentStr.startsWith('@gate ')) {
|
|
const code = commentStr.slice(6);
|
|
const ctxIdentifier = t.identifier('ctx');
|
|
const condition = parse(code, ctxIdentifier);
|
|
if (conditions === null) {
|
|
conditions = [condition];
|
|
} else {
|
|
conditions.push(condition);
|
|
}
|
|
}
|
|
}
|
|
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 = getComments(path);
|
|
if (comments !== undefined) {
|
|
const condition = buildGateCondition(comments);
|
|
if (condition !== null) {
|
|
callee.name =
|
|
callee.name === 'fit' ? '_test_gate_focus' : '_test_gate';
|
|
expression.arguments = [
|
|
t.arrowFunctionExpression(
|
|
[t.identifier('ctx')],
|
|
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 = getComments(path);
|
|
if (comments !== undefined) {
|
|
const condition = buildGateCondition(comments);
|
|
if (condition !== null) {
|
|
statement.expression = t.callExpression(
|
|
t.identifier('_test_gate_focus'),
|
|
[
|
|
t.arrowFunctionExpression(
|
|
[t.identifier('ctx')],
|
|
condition
|
|
),
|
|
...expression.arguments,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = transform;
|