• 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 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