• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2020 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Mutation Db.
7 */
8
9const crypto = require('crypto');
10const fs = require('fs');
11const fsPath = require('path');
12
13const babelGenerator = require('@babel/generator').default;
14const babelTemplate = require('@babel/template').default;
15const babelTraverse = require('@babel/traverse').default;
16const babelTypes = require('@babel/types');
17const globals = require('globals');
18
19const random = require('./random.js');
20const sourceHelpers = require('./source_helpers.js');
21
22const globalIdentifiers = new Set(Object.keys(globals.builtin));
23const propertyNames = new Set([
24    // Parsed from https://github.com/tc39/ecma262/blob/master/spec.html
25    'add',
26    'anchor',
27    'apply',
28    'big',
29    'bind',
30    'blink',
31    'bold',
32    'buffer',
33    'byteLength',
34    'byteOffset',
35    'BYTES_PER_ELEMENT',
36    'call',
37    'catch',
38    'charAt',
39    'charCodeAt',
40    'clear',
41    'codePointAt',
42    'compile',
43    'concat',
44    'constructor',
45    'copyWithin',
46    '__defineGetter__',
47    '__defineSetter__',
48    'delete',
49    'endsWith',
50    'entries',
51    'every',
52    'exec',
53    'fill',
54    'filter',
55    'find',
56    'findIndex',
57    'fixed',
58    'flags',
59    'fontcolor',
60    'fontsize',
61    'forEach',
62    'get',
63    'getDate',
64    'getDay',
65    'getFloat32',
66    'getFloat64',
67    'getFullYear',
68    'getHours',
69    'getInt16',
70    'getInt32',
71    'getInt8',
72    'getMilliseconds',
73    'getMinutes',
74    'getMonth',
75    'getSeconds',
76    'getTime',
77    'getTimezoneOffset',
78    'getUint16',
79    'getUint32',
80    'getUint8',
81    'getUTCDate',
82    'getUTCDay',
83    'getUTCFullYear',
84    'getUTCHours',
85    'getUTCMilliseconds',
86    'getUTCMinutes',
87    'getUTCMonth',
88    'getUTCSeconds',
89    'getYear',
90    'global',
91    'has',
92    'hasInstance',
93    'hasOwnProperty',
94    'ignoreCase',
95    'includes',
96    'indexOf',
97    'isConcatSpreadable',
98    'isPrototypeOf',
99    'italics',
100    'iterator',
101    'join',
102    'keys',
103    'lastIndexOf',
104    'length',
105    'link',
106    'localeCompare',
107    '__lookupGetter__',
108    '__lookupSetter__',
109    'map',
110    'match',
111    'match',
112    'message',
113    'multiline',
114    'name',
115    'next',
116    'normalize',
117    'padEnd',
118    'padStart',
119    'pop',
120    'propertyIsEnumerable',
121    '__proto__',
122    'prototype',
123    'push',
124    'reduce',
125    'reduceRight',
126    'repeat',
127    'replace',
128    'replace',
129    'return',
130    'reverse',
131    'search',
132    'search',
133    'set',
134    'set',
135    'setDate',
136    'setFloat32',
137    'setFloat64',
138    'setFullYear',
139    'setHours',
140    'setInt16',
141    'setInt32',
142    'setInt8',
143    'setMilliseconds',
144    'setMinutes',
145    'setMonth',
146    'setSeconds',
147    'setTime',
148    'setUint16',
149    'setUint32',
150    'setUint8',
151    'setUTCDate',
152    'setUTCFullYear',
153    'setUTCHours',
154    'setUTCMilliseconds',
155    'setUTCMinutes',
156    'setUTCMonth',
157    'setUTCSeconds',
158    'setYear',
159    'shift',
160    'size',
161    'slice',
162    'slice',
163    'small',
164    'some',
165    'sort',
166    'source',
167    'species',
168    'splice',
169    'split',
170    'split',
171    'startsWith',
172    'sticky',
173    'strike',
174    'sub',
175    'subarray',
176    'substr',
177    'substring',
178    'sup',
179    'test',
180    'then',
181    'throw',
182    'toDateString',
183    'toExponential',
184    'toFixed',
185    'toGMTString',
186    'toISOString',
187    'toJSON',
188    'toLocaleDateString',
189    'toLocaleLowerCase',
190    'toLocaleString',
191    'toLocaleTimeString',
192    'toLocaleUpperCase',
193    'toLowerCase',
194    'toPrecision',
195    'toPrimitive',
196    'toString',
197    'toStringTag',
198    'toTimeString',
199    'toUpperCase',
200    'toUTCString',
201    'trim',
202    'unicode',
203    'unscopables',
204    'unshift',
205    'valueOf',
206    'values',
207]);
208
209const MAX_DEPENDENCIES = 2;
210
211class Expression {
212  constructor(type, source, isStatement, originalPath,
213              dependencies, needsSuper) {
214    this.type = type;
215    this.source = source;
216    this.isStatement = isStatement;
217    this.originalPath = originalPath;
218    this.dependencies = dependencies;
219    this.needsSuper = needsSuper;
220  }
221}
222
223function dedupKey(expression) {
224  if (!expression.dependencies) {
225    return expression.source;
226  }
227
228  let result = expression.source;
229  for (let dependency of expression.dependencies) {
230    result = result.replace(new RegExp(dependency, 'g'), 'ID');
231  }
232
233  return result;
234}
235
236function _markSkipped(path) {
237  while (path) {
238    path.node.__skipped = true;
239    path = path.parentPath;
240  }
241}
242
243/**
244 * Returns true if an expression can be applied or false otherwise.
245 */
246function isValid(expression) {
247  const expressionTemplate = babelTemplate(
248      expression.source,
249      sourceHelpers.BABYLON_REPLACE_VAR_OPTIONS);
250
251  const dependencies = {};
252  if (expression.dependencies) {
253    for (const dependency of expression.dependencies) {
254      dependencies[dependency] = babelTypes.identifier('__v_0');
255    }
256  }
257
258  try {
259    expressionTemplate(dependencies);
260  } catch (e) {
261    return false;
262  }
263  return true;
264}
265
266class MutateDbWriter {
267  constructor(outputDir) {
268    this.seen = new Set();
269    this.outputDir = fsPath.resolve(outputDir);
270    this.index = {
271      statements: [],
272      superStatements: [],
273      all: [],
274    };
275  }
276
277  process(source) {
278    let self = this;
279
280    let varIndex = 0;
281
282    // First pass to collect dependency information.
283    babelTraverse(source.ast, {
284      Super(path) {
285        while (path) {
286          path.node.__needsSuper = true;
287          path = path.parentPath;
288        }
289      },
290
291      YieldExpression(path) {
292        // Don't include yield expressions in DB.
293        _markSkipped(path);
294      },
295
296      Identifier(path) {
297        if (globalIdentifiers.has(path.node.name) &&
298            path.node.name != 'eval') {
299          // Global name.
300          return;
301        }
302
303        if (propertyNames.has(path.node.name) &&
304            path.parentPath.isMemberExpression() &&
305            path.parentKey !== 'object') {
306          // Builtin property name.
307          return;
308        }
309
310        let binding = path.scope.getBinding(path.node.name);
311        if (!binding) {
312          // Unknown dependency. Don't handle this.
313          _markSkipped(path);
314          return;
315        }
316
317        let newName;
318        if (path.node.name.startsWith('VAR_')) {
319          newName = path.node.name;
320        } else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
321                   babelTypes.isFunctionExpression(binding.path.node) ||
322                   babelTypes.isDeclaration(binding.path.node) ||
323                   babelTypes.isFunctionExpression(binding.path.node)) {
324          // Unknown dependency. Don't handle this.
325          _markSkipped(path);
326          return;
327        } else {
328          newName = 'VAR_' + varIndex++;
329          path.scope.rename(path.node.name, newName);
330        }
331
332        // Mark all parents as having a dependency.
333        while (path) {
334          path.node.__idDependencies = path.node.__idDependencies || [];
335          if (path.node.__idDependencies.length <= MAX_DEPENDENCIES) {
336            path.node.__idDependencies.push(newName);
337          }
338          path = path.parentPath;
339        }
340      }
341    });
342
343    babelTraverse(source.ast, {
344      Expression(path) {
345        if (!path.parentPath.isExpressionStatement()) {
346          return;
347        }
348
349        if (path.node.__skipped ||
350            (path.node.__idDependencies &&
351             path.node.__idDependencies.length > MAX_DEPENDENCIES)) {
352          return;
353        }
354
355        if (path.isIdentifier() || path.isMemberExpression() ||
356            path.isConditionalExpression() ||
357            path.isBinaryExpression() || path.isDoExpression() ||
358            path.isLiteral() ||
359            path.isObjectExpression() || path.isArrayExpression()) {
360          // Skip:
361          //   - Identifiers.
362          //   - Member expressions (too many and too context dependent).
363          //   - Conditional expressions (too many and too context dependent).
364          //   - Binary expressions (too many).
365          //   - Literals (too many).
366          //   - Object/array expressions (too many).
367          return;
368        }
369
370        if (path.isAssignmentExpression()) {
371          if (!babelTypes.isMemberExpression(path.node.left)) {
372            // Skip assignments that aren't to properties.
373            return;
374          }
375
376          if (babelTypes.isIdentifier(path.node.left.object)) {
377            if (babelTypes.isNumericLiteral(path.node.left.property)) {
378              // Skip VAR[\d+] = ...;
379              // There are too many and they generally aren't very useful.
380              return;
381            }
382
383            if (babelTypes.isStringLiteral(path.node.left.property) &&
384                !propertyNames.has(path.node.left.property.value)) {
385              // Skip custom properties. e.g.
386              // VAR["abc"] = ...;
387              // There are too many and they generally aren't very useful.
388              return;
389            }
390          }
391        }
392
393        if (path.isCallExpression() &&
394            babelTypes.isIdentifier(path.node.callee) &&
395            !globalIdentifiers.has(path.node.callee.name)) {
396          // Skip VAR(...) calls since there's too much context we're missing.
397          return;
398        }
399
400        if (path.isUnaryExpression() && path.node.operator == '-') {
401          // Skip -... since there are too many.
402          return;
403        }
404
405        // Make the template.
406        let generated = babelGenerator(path.node, { concise: true }).code;
407        let expression = new Expression(
408            path.node.type,
409            generated,
410            path.parentPath.isExpressionStatement(),
411            source.relPath,
412            path.node.__idDependencies,
413            Boolean(path.node.__needsSuper));
414
415        // Try to de-dupe similar expressions.
416        let key = dedupKey(expression);
417        if (self.seen.has(key)) {
418          return;
419        }
420
421        // Test results.
422        if (!isValid(expression)) {
423          return;
424        }
425
426        // Write results.
427        let dirPath = fsPath.join(self.outputDir, expression.type);
428        if (!fs.existsSync(dirPath)) {
429          fs.mkdirSync(dirPath);
430        }
431
432        let sha1sum = crypto.createHash('sha1');
433        sha1sum.update(key);
434
435        let filePath = fsPath.join(dirPath, sha1sum.digest('hex') + '.json');
436        fs.writeFileSync(filePath, JSON.stringify(expression));
437
438        let relPath = fsPath.relative(self.outputDir, filePath);
439
440        // Update index.
441        self.seen.add(key);
442        self.index.all.push(relPath);
443
444        if (expression.needsSuper) {
445          self.index.superStatements.push(relPath);
446        } else {
447          self.index.statements.push(relPath);
448        }
449      }
450    });
451  }
452
453  writeIndex() {
454    fs.writeFileSync(
455        fsPath.join(this.outputDir, 'index.json'),
456        JSON.stringify(this.index));
457  }
458}
459
460class MutateDb {
461  constructor(outputDir) {
462    this.outputDir = fsPath.resolve(outputDir);
463    this.index = JSON.parse(
464        fs.readFileSync(fsPath.join(outputDir, 'index.json'), 'utf-8'));
465  }
466
467  getRandomStatement({canHaveSuper=false} = {}) {
468    let choices;
469    if (canHaveSuper) {
470      choices = random.randInt(0, 1) ?
471          this.index.all : this.index.superStatements;
472    } else {
473      choices = this.index.statements;
474    }
475
476    let path = fsPath.join(
477        this.outputDir, choices[random.randInt(0, choices.length - 1)]);
478    return JSON.parse(fs.readFileSync(path), 'utf-8');
479  }
480}
481
482module.exports = {
483  MutateDb: MutateDb,
484  MutateDbWriter: MutateDbWriter,
485}
486