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 Source loader. 7 */ 8 9const fs = require('fs'); 10const fsPath = require('path'); 11 12const { EOL } = require('os'); 13 14const babelGenerator = require('@babel/generator').default; 15const babelTraverse = require('@babel/traverse').default; 16const babelTypes = require('@babel/types'); 17const babylon = require('@babel/parser'); 18 19const exceptions = require('./exceptions.js'); 20 21const SCRIPT = Symbol('SCRIPT'); 22const MODULE = Symbol('MODULE'); 23 24const V8_BUILTIN_PREFIX = '__V8Builtin'; 25const V8_REPLACE_BUILTIN_REGEXP = new RegExp( 26 V8_BUILTIN_PREFIX + '(\\w+)\\(', 'g'); 27 28const BABYLON_OPTIONS = { 29 sourceType: 'script', 30 allowReturnOutsideFunction: true, 31 tokens: false, 32 ranges: false, 33 plugins: [ 34 'asyncGenerators', 35 'bigInt', 36 'classPrivateMethods', 37 'classPrivateProperties', 38 'classProperties', 39 'doExpressions', 40 'exportDefaultFrom', 41 'nullishCoalescingOperator', 42 'numericSeparator', 43 'objectRestSpread', 44 'optionalCatchBinding', 45 'optionalChaining', 46 ], 47} 48 49const BABYLON_REPLACE_VAR_OPTIONS = Object.assign({}, BABYLON_OPTIONS); 50BABYLON_REPLACE_VAR_OPTIONS['placeholderPattern'] = /^VAR_[0-9]+$/; 51 52function _isV8OrSpiderMonkeyLoad(path) { 53 // 'load' and 'loadRelativeToScript' used by V8 and SpiderMonkey. 54 return (babelTypes.isIdentifier(path.node.callee) && 55 (path.node.callee.name == 'load' || 56 path.node.callee.name == 'loadRelativeToScript') && 57 path.node.arguments.length == 1 && 58 babelTypes.isStringLiteral(path.node.arguments[0])); 59} 60 61function _isChakraLoad(path) { 62 // 'WScript.LoadScriptFile' used by Chakra. 63 // TODO(ochang): The optional second argument can change semantics ("self", 64 // "samethread", "crossthread" etc). 65 // Investigate whether if it still makes sense to include them. 66 return (babelTypes.isMemberExpression(path.node.callee) && 67 babelTypes.isIdentifier(path.node.callee.property) && 68 path.node.callee.property.name == 'LoadScriptFile' && 69 path.node.arguments.length >= 1 && 70 babelTypes.isStringLiteral(path.node.arguments[0])); 71} 72 73function _findPath(path, caseSensitive=true) { 74 // If the path exists, return the path. Otherwise return null. Used to handle 75 // case insensitive matches for Chakra tests. 76 if (caseSensitive) { 77 return fs.existsSync(path) ? path : null; 78 } 79 80 path = fsPath.normalize(fsPath.resolve(path)); 81 const pathComponents = path.split(fsPath.sep); 82 let realPath = fsPath.resolve(fsPath.sep); 83 84 for (let i = 1; i < pathComponents.length; i++) { 85 // For each path component, do a directory listing to see if there is a case 86 // insensitive match. 87 const curListing = fs.readdirSync(realPath); 88 let realComponent = null; 89 for (const component of curListing) { 90 if (i < pathComponents.length - 1 && 91 !fs.statSync(fsPath.join(realPath, component)).isDirectory()) { 92 continue; 93 } 94 95 if (component.toLowerCase() == pathComponents[i].toLowerCase()) { 96 realComponent = component; 97 break; 98 } 99 } 100 101 if (!realComponent) { 102 return null; 103 } 104 105 realPath = fsPath.join(realPath, realComponent); 106 } 107 108 return realPath; 109} 110 111function _findDependentCodePath(filePath, baseDirectory, caseSensitive=true) { 112 const fullPath = fsPath.join(baseDirectory, filePath); 113 114 const realPath = _findPath(fullPath, caseSensitive) 115 if (realPath) { 116 // Check base directory of current file. 117 return realPath; 118 } 119 120 while (fsPath.dirname(baseDirectory) != baseDirectory) { 121 // Walk up the directory tree. 122 const testPath = fsPath.join(baseDirectory, filePath); 123 const realPath = _findPath(testPath, caseSensitive) 124 if (realPath) { 125 return realPath; 126 } 127 128 baseDirectory = fsPath.dirname(baseDirectory); 129 } 130 131 return null; 132} 133 134/** 135 * Removes V8/Spidermonkey/Chakra load expressions in a source AST and returns 136 * their string values in an array. 137 * 138 * @param {string} originalFilePath Absolute path to file. 139 * @param {AST} ast Babel AST of the sources. 140 */ 141function resolveLoads(originalFilePath, ast) { 142 const dependencies = []; 143 144 babelTraverse(ast, { 145 CallExpression(path) { 146 const isV8OrSpiderMonkeyLoad = _isV8OrSpiderMonkeyLoad(path); 147 const isChakraLoad = _isChakraLoad(path); 148 if (!isV8OrSpiderMonkeyLoad && !isChakraLoad) { 149 return; 150 } 151 152 let loadValue = path.node.arguments[0].extra.rawValue; 153 // Normalize Windows path separators. 154 loadValue = loadValue.replace(/\\/g, fsPath.sep); 155 156 // Remove load call. 157 path.remove(); 158 159 const resolvedPath = _findDependentCodePath( 160 loadValue, fsPath.dirname(originalFilePath), !isChakraLoad); 161 if (!resolvedPath) { 162 console.log('ERROR: Could not find dependent path for', loadValue); 163 return; 164 } 165 166 if (exceptions.isTestSkippedAbs(resolvedPath)) { 167 // Dependency is skipped. 168 return; 169 } 170 171 // Add the dependency path. 172 dependencies.push(resolvedPath); 173 } 174 }); 175 return dependencies; 176} 177 178function isStrictDirective(directive) { 179 return (directive.value && 180 babelTypes.isDirectiveLiteral(directive.value) && 181 directive.value.value === 'use strict'); 182} 183 184function replaceV8Builtins(code) { 185 return code.replace(/%(\w+)\(/g, V8_BUILTIN_PREFIX + '$1('); 186} 187 188function restoreV8Builtins(code) { 189 return code.replace(V8_REPLACE_BUILTIN_REGEXP, '%$1('); 190} 191 192function maybeUseStict(code, useStrict) { 193 if (useStrict) { 194 return `'use strict';${EOL}${EOL}${code}`; 195 } 196 return code; 197} 198 199class Source { 200 constructor(baseDir, relPath, flags, dependentPaths) { 201 this.baseDir = baseDir; 202 this.relPath = relPath; 203 this.flags = flags; 204 this.dependentPaths = dependentPaths; 205 this.sloppy = exceptions.isTestSloppyRel(relPath); 206 } 207 208 get absPath() { 209 return fsPath.join(this.baseDir, this.relPath); 210 } 211 212 /** 213 * Specifies if the source isn't compatible with strict mode. 214 */ 215 isSloppy() { 216 return this.sloppy; 217 } 218 219 /** 220 * Specifies if the source has a top-level 'use strict' directive. 221 */ 222 isStrict() { 223 throw Error('Not implemented'); 224 } 225 226 /** 227 * Generates the code as a string without any top-level 'use strict' 228 * directives. V8 natives that were replaced before parsing are restored. 229 */ 230 generateNoStrict() { 231 throw Error('Not implemented'); 232 } 233 234 /** 235 * Recursively adds dependencies of a this source file. 236 * 237 * @param {Map} dependencies Dependency map to which to add new, parsed 238 * dependencies unless they are already in the map. 239 * @param {Map} visitedDependencies A set for avoiding loops. 240 */ 241 loadDependencies(dependencies, visitedDependencies) { 242 visitedDependencies = visitedDependencies || new Set(); 243 244 for (const absPath of this.dependentPaths) { 245 if (dependencies.has(absPath) || 246 visitedDependencies.has(absPath)) { 247 // Already added. 248 continue; 249 } 250 251 // Prevent infinite loops. 252 visitedDependencies.add(absPath); 253 254 // Recursively load dependencies. 255 const dependency = loadDependencyAbs(this.baseDir, absPath); 256 dependency.loadDependencies(dependencies, visitedDependencies); 257 258 // Add the dependency. 259 dependencies.set(absPath, dependency); 260 } 261 } 262} 263 264/** 265 * Represents sources whose AST can be manipulated. 266 */ 267class ParsedSource extends Source { 268 constructor(ast, baseDir, relPath, flags, dependentPaths) { 269 super(baseDir, relPath, flags, dependentPaths); 270 this.ast = ast; 271 } 272 273 isStrict() { 274 return !!this.ast.program.directives.filter(isStrictDirective).length; 275 } 276 277 generateNoStrict() { 278 const allDirectives = this.ast.program.directives; 279 this.ast.program.directives = this.ast.program.directives.filter( 280 directive => !isStrictDirective(directive)); 281 try { 282 const code = babelGenerator(this.ast.program, {comments: true}).code; 283 return restoreV8Builtins(code); 284 } finally { 285 this.ast.program.directives = allDirectives; 286 } 287 } 288} 289 290/** 291 * Represents sources with cached code. 292 */ 293class CachedSource extends Source { 294 constructor(source) { 295 super(source.baseDir, source.relPath, source.flags, source.dependentPaths); 296 this.use_strict = source.isStrict(); 297 this.code = source.generateNoStrict(); 298 } 299 300 isStrict() { 301 return this.use_strict; 302 } 303 304 generateNoStrict() { 305 return this.code; 306 } 307} 308 309/** 310 * Read file path into an AST. 311 * 312 * Post-processes the AST by replacing V8 natives and removing disallowed 313 * natives, as well as removing load expressions and adding the paths-to-load 314 * as meta data. 315 */ 316function loadSource(baseDir, relPath, parseStrict=false) { 317 const absPath = fsPath.resolve(fsPath.join(baseDir, relPath)); 318 const data = fs.readFileSync(absPath, 'utf-8'); 319 320 if (guessType(data) !== SCRIPT) { 321 return null; 322 } 323 324 const preprocessed = maybeUseStict(replaceV8Builtins(data), parseStrict); 325 const ast = babylon.parse(preprocessed, BABYLON_OPTIONS); 326 327 removeComments(ast); 328 cleanAsserts(ast); 329 annotateWithOriginalPath(ast, relPath); 330 331 const flags = loadFlags(data); 332 const dependentPaths = resolveLoads(absPath, ast); 333 334 return new ParsedSource(ast, baseDir, relPath, flags, dependentPaths); 335} 336 337function guessType(data) { 338 if (data.includes('// MODULE')) { 339 return MODULE; 340 } 341 342 return SCRIPT; 343} 344 345/** 346 * Remove existing comments. 347 */ 348function removeComments(ast) { 349 babelTraverse(ast, { 350 enter(path) { 351 babelTypes.removeComments(path.node); 352 } 353 }); 354} 355 356/** 357 * Removes "Assert" from strings in spidermonkey shells or from older 358 * crash tests: https://crbug.com/1068268 359 */ 360function cleanAsserts(ast) { 361 function replace(string) { 362 return string.replace(/[Aa]ssert/g, '*****t'); 363 } 364 babelTraverse(ast, { 365 StringLiteral(path) { 366 path.node.value = replace(path.node.value); 367 path.node.extra.raw = replace(path.node.extra.raw); 368 path.node.extra.rawValue = replace(path.node.extra.rawValue); 369 }, 370 TemplateElement(path) { 371 path.node.value.cooked = replace(path.node.value.cooked); 372 path.node.value.raw = replace(path.node.value.raw); 373 }, 374 }); 375} 376 377/** 378 * Annotate code with original file path. 379 */ 380function annotateWithOriginalPath(ast, relPath) { 381 if (ast.program && ast.program.body && ast.program.body.length > 0) { 382 babelTypes.addComment( 383 ast.program.body[0], 'leading', ' Original: ' + relPath, true); 384 } 385} 386 387// TODO(machenbach): Move this into the V8 corpus. Other test suites don't 388// use this flag logic. 389function loadFlags(data) { 390 const result = []; 391 let count = 0; 392 for (const line of data.split('\n')) { 393 if (count++ > 40) { 394 // No need to process the whole file. Flags are always added after the 395 // copyright header. 396 break; 397 } 398 const match = line.match(/\/\/ Flags:\s*(.*)\s*/); 399 if (!match) { 400 continue; 401 } 402 for (const flag of exceptions.filterFlags(match[1].split(/\s+/))) { 403 result.push(flag); 404 } 405 } 406 return result; 407} 408 409// Convenience helper to load sources with absolute paths. 410function loadSourceAbs(baseDir, absPath) { 411 return loadSource(baseDir, fsPath.relative(baseDir, absPath)); 412} 413 414const dependencyCache = new Map(); 415 416function loadDependency(baseDir, relPath) { 417 const absPath = fsPath.join(baseDir, relPath); 418 let dependency = dependencyCache.get(absPath); 419 if (!dependency) { 420 const source = loadSource(baseDir, relPath); 421 dependency = new CachedSource(source); 422 dependencyCache.set(absPath, dependency); 423 } 424 return dependency; 425} 426 427function loadDependencyAbs(baseDir, absPath) { 428 return loadDependency(baseDir, fsPath.relative(baseDir, absPath)); 429} 430 431// Convenience helper to load a file from the resources directory. 432function loadResource(fileName) { 433 return loadDependency(__dirname, fsPath.join('resources', fileName)); 434} 435 436function generateCode(source, dependencies=[]) { 437 const allSources = dependencies.concat([source]); 438 const codePieces = allSources.map( 439 source => source.generateNoStrict()); 440 441 if (allSources.some(source => source.isStrict()) && 442 !allSources.some(source => source.isSloppy())) { 443 codePieces.unshift('\'use strict\';'); 444 } 445 446 return codePieces.join(EOL + EOL); 447} 448 449module.exports = { 450 BABYLON_OPTIONS: BABYLON_OPTIONS, 451 BABYLON_REPLACE_VAR_OPTIONS: BABYLON_REPLACE_VAR_OPTIONS, 452 generateCode: generateCode, 453 loadDependencyAbs: loadDependencyAbs, 454 loadResource: loadResource, 455 loadSource: loadSource, 456 loadSourceAbs: loadSourceAbs, 457 ParsedSource: ParsedSource, 458} 459