main
js 336 lines 8.79 KB
Raw
1 'use strict';
2
3 /* eslint-disable no-for-of-loops/no-for-of-loops */
4
5 const getComments = require('./getComments');
6
7 function transform(babel) {
8 const {types: t} = babel;
9
10 // A very stupid subset of pseudo-JavaScript, used to run tests conditionally
11 // based on the environment.
12 //
13 // Input:
14 // @gate a && (b || c)
15 // test('some test', () => {/*...*/})
16 //
17 // Output:
18 // @gate a && (b || c)
19 // _test_gate(ctx => ctx.a && (ctx.b || ctx.c), 'some test', () => {/*...*/});
20 //
21 // expression → binary ( ( "||" | "&&" ) binary)* ;
22 // binary → unary ( ( "==" | "!=" | "===" | "!==" ) unary )* ;
23 // unary → "!" primary
24 // | primary ;
25 // primary → NAME | STRING | BOOLEAN
26 // | "(" expression ")" ;
27 function tokenize(code) {
28 const tokens = [];
29 let i = 0;
30 while (i < code.length) {
31 let char = code[i];
32 // Double quoted strings
33 if (char === '"') {
34 let string = '';
35 i++;
36 do {
37 if (i > code.length) {
38 throw Error('Missing a closing quote');
39 }
40 char = code[i++];
41 if (char === '"') {
42 break;
43 }
44 string += char;
45 } while (true);
46 tokens.push({type: 'string', value: string});
47 continue;
48 }
49
50 // Single quoted strings
51 if (char === "'") {
52 let string = '';
53 i++;
54 do {
55 if (i > code.length) {
56 throw Error('Missing a closing quote');
57 }
58 char = code[i++];
59 if (char === "'") {
60 break;
61 }
62 string += char;
63 } while (true);
64 tokens.push({type: 'string', value: string});
65 continue;
66 }
67
68 // Whitespace
69 if (/\s/.test(char)) {
70 if (char === '\n') {
71 return tokens;
72 }
73 i++;
74 continue;
75 }
76
77 const next3 = code.slice(i, i + 3);
78 if (next3 === '===') {
79 tokens.push({type: '=='});
80 i += 3;
81 continue;
82 }
83 if (next3 === '!==') {
84 tokens.push({type: '!='});
85 i += 3;
86 continue;
87 }
88
89 const next2 = code.slice(i, i + 2);
90 switch (next2) {
91 case '&&':
92 case '||':
93 case '==':
94 case '!=':
95 tokens.push({type: next2});
96 i += 2;
97 continue;
98 case '//':
99 // This is the beginning of a line comment. The rest of the line
100 // is ignored.
101 return tokens;
102 }
103
104 switch (char) {
105 case '(':
106 case ')':
107 case '!':
108 tokens.push({type: char});
109 i++;
110 continue;
111 }
112
113 // Names
114 const nameRegex = /[a-zA-Z_$][0-9a-zA-Z_$]*/y;
115 nameRegex.lastIndex = i;
116 const match = nameRegex.exec(code);
117 if (match !== null) {
118 const name = match[0];
119 switch (name) {
120 case 'true': {
121 tokens.push({type: 'boolean', value: true});
122 break;
123 }
124 case 'false': {
125 tokens.push({type: 'boolean', value: false});
126 break;
127 }
128 default: {
129 tokens.push({type: 'name', name});
130 }
131 }
132 i += name.length;
133 continue;
134 }
135
136 throw Error('Invalid character: ' + char);
137 }
138 return tokens;
139 }
140
141 function parse(code, ctxIdentifier) {
142 const tokens = tokenize(code);
143
144 let i = 0;
145 function parseExpression() {
146 let left = parseBinary();
147 while (true) {
148 const token = tokens[i];
149 if (token !== undefined) {
150 switch (token.type) {
151 case '||':
152 case '&&': {
153 i++;
154 const right = parseBinary();
155 if (right === null) {
156 throw Error('Missing expression after ' + token.type);
157 }
158 left = t.logicalExpression(token.type, left, right);
159 continue;
160 }
161 }
162 }
163 break;
164 }
165 return left;
166 }
167
168 function parseBinary() {
169 let left = parseUnary();
170 while (true) {
171 const token = tokens[i];
172 if (token !== undefined) {
173 switch (token.type) {
174 case '==':
175 case '!=': {
176 i++;
177 const right = parseUnary();
178 if (right === null) {
179 throw Error('Missing expression after ' + token.type);
180 }
181 left = t.binaryExpression(token.type, left, right);
182 continue;
183 }
184 }
185 }
186 break;
187 }
188 return left;
189 }
190
191 function parseUnary() {
192 const token = tokens[i];
193 if (token !== undefined) {
194 if (token.type === '!') {
195 i++;
196 const argument = parseUnary();
197 return t.unaryExpression('!', argument);
198 }
199 }
200 return parsePrimary();
201 }
202
203 function parsePrimary() {
204 const token = tokens[i];
205 switch (token.type) {
206 case 'boolean': {
207 i++;
208 return t.booleanLiteral(token.value);
209 }
210 case 'name': {
211 i++;
212 return t.memberExpression(ctxIdentifier, t.identifier(token.name));
213 }
214 case 'string': {
215 i++;
216 return t.stringLiteral(token.value);
217 }
218 case '(': {
219 i++;
220 const expression = parseExpression();
221 const closingParen = tokens[i];
222 if (closingParen === undefined || closingParen.type !== ')') {
223 throw Error('Expected closing )');
224 }
225 i++;
226 return expression;
227 }
228 default: {
229 throw Error('Unexpected token: ' + token.type);
230 }
231 }
232 }
233
234 const program = parseExpression();
235 if (tokens[i] !== undefined) {
236 throw Error('Unexpected token');
237 }
238 return program;
239 }
240
241 function buildGateCondition(comments) {
242 let conditions = null;
243 for (const line of comments) {
244 const commentStr = line.value.trim();
245 if (commentStr.startsWith('@gate ')) {
246 const code = commentStr.slice(6);
247 const ctxIdentifier = t.identifier('ctx');
248 const condition = parse(code, ctxIdentifier);
249 if (conditions === null) {
250 conditions = [condition];
251 } else {
252 conditions.push(condition);
253 }
254 }
255 }
256 if (conditions !== null) {
257 let condition = conditions[0];
258 for (let i = 1; i < conditions.length; i++) {
259 const right = conditions[i];
260 condition = t.logicalExpression('&&', condition, right);
261 }
262 return condition;
263 } else {
264 return null;
265 }
266 }
267
268 return {
269 name: 'test-gate-pragma',
270 visitor: {
271 ExpressionStatement(path) {
272 const statement = path.node;
273 const expression = statement.expression;
274 if (expression.type === 'CallExpression') {
275 const callee = expression.callee;
276 switch (callee.type) {
277 case 'Identifier': {
278 if (
279 callee.name === 'test' ||
280 callee.name === 'it' ||
281 callee.name === 'fit'
282 ) {
283 const comments = getComments(path);
284 if (comments !== undefined) {
285 const condition = buildGateCondition(comments);
286 if (condition !== null) {
287 callee.name =
288 callee.name === 'fit' ? '_test_gate_focus' : '_test_gate';
289 expression.arguments = [
290 t.arrowFunctionExpression(
291 [t.identifier('ctx')],
292 condition
293 ),
294 ...expression.arguments,
295 ];
296 }
297 }
298 }
299 break;
300 }
301 case 'MemberExpression': {
302 if (
303 callee.object.type === 'Identifier' &&
304 (callee.object.name === 'test' ||
305 callee.object.name === 'it') &&
306 callee.property.type === 'Identifier' &&
307 callee.property.name === 'only'
308 ) {
309 const comments = getComments(path);
310 if (comments !== undefined) {
311 const condition = buildGateCondition(comments);
312 if (condition !== null) {
313 statement.expression = t.callExpression(
314 t.identifier('_test_gate_focus'),
315 [
316 t.arrowFunctionExpression(
317 [t.identifier('ctx')],
318 condition
319 ),
320 ...expression.arguments,
321 ]
322 );
323 }
324 }
325 }
326 break;
327 }
328 }
329 }
330 return;
331 },
332 },
333 };
334 }
335
336 module.exports = transform;