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