metro-bundler: collect-dependencies: harden requirements for require()

Reviewed By: davidaurelio

Differential Revision: D5658522

fbshipit-source-id: 2970d62bcb18d57595c666d680407a23eccda77b
This commit is contained in:
Jean Lauliac 2017-08-21 04:23:16 -07:00 committed by Facebook Github Bot
parent 6a0efc0853
commit bf25e49665
2 changed files with 64 additions and 26 deletions

View File

@ -18,6 +18,8 @@ const {codeFromAst, comparableCode} = require('../../test-helpers');
const {any} = expect;
const {InvalidRequireCallError} = collectDependencies;
describe('dependency collection from ASTs:', () => {
it('collects dependency identifiers from the code', () => {
const ast = astFromCode(`
@ -41,16 +43,20 @@ describe('dependency collection from ASTs:', () => {
expect(collectDependencies(ast).dependencies).toEqual(['left-pad']);
});
it('ignores template literals with interpolations', () => {
it('throws on template literals with interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)');
expect(collectDependencies(ast).dependencies).toEqual([]);
expect(() => collectDependencies(ast).dependencies).toThrowError(
InvalidRequireCallError,
);
});
it('ignores tagged template literals', () => {
it('throws on tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
expect(collectDependencies(ast).dependencies).toEqual([]);
expect(() => collectDependencies(ast).dependencies).toThrowError(
InvalidRequireCallError,
);
});
it('exposes a string as `dependencyMapName`', () => {
@ -66,7 +72,6 @@ describe('dependency collection from ASTs:', () => {
it('replaces all required module ID strings with array lookups, keeps the ID as second argument', () => {
const ast = astFromCode(`
const a = require('b/lib/a');
const b = require(123);
exports.do = () => require("do");
if (!something) {
require("setup/something");
@ -78,7 +83,6 @@ describe('dependency collection from ASTs:', () => {
expect(codeFromAst(ast)).toEqual(
comparableCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
@ -96,7 +100,6 @@ describe('Dependency collection from optimized ASTs:', () => {
beforeEach(() => {
ast = astFromCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
@ -126,7 +129,6 @@ describe('Dependency collection from optimized ASTs:', () => {
expect(codeFromAst(ast)).toEqual(
comparableCode(`
const a = require(${dependencyMapName}[0]);
const b = require(123);
exports.do = () => require(${dependencyMapName}[1]);
if (!something) {
require(${dependencyMapName}[2]);

View File

@ -15,6 +15,7 @@
const nullthrows = require('fbjs/lib/nullthrows');
const {traverse, types} = require('babel-core');
const prettyPrint = require('babel-generator').default;
type AST = Object;
@ -27,13 +28,16 @@ class Replacement {
this.nextIndex = 0;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' &&
callee.name === 'require' &&
firstArg &&
isLiteralString(firstArg)
);
getRequireCallArg(node) {
const args = node.arguments;
if (args.length !== 1 || !isLiteralString(args[0])) {
throw new InvalidRequireCallError(
'Calls to require() expect exactly 1 string literal argument, but ' +
'this was found: ' +
prettyPrint(node).code,
);
}
return args[0];
}
getIndex(stringLiteralOrTemplateLiteral) {
@ -59,6 +63,14 @@ class Replacement {
}
}
function getInvalidProdRequireMessage(node) {
return (
'Post-transform calls to require() expect 2 arguments, the first ' +
'of which has the shape `_dependencyMapName[123]`, but this was found: ' +
prettyPrint(node).code
);
}
class ProdReplacement {
replacement: Replacement;
names: Array<string>;
@ -68,15 +80,22 @@ class ProdReplacement {
this.names = names;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' &&
callee.name === 'require' &&
firstArg &&
firstArg.type === 'MemberExpression' &&
firstArg.property &&
firstArg.property.type === 'NumericLiteral'
);
getRequireCallArg(node) {
const args = node.arguments;
if (args.length !== 2) {
throw new InvalidRequireCallError(getInvalidProdRequireMessage(node));
}
const arg = args[0];
if (
!(
arg.type === 'MemberExpression' &&
arg.property &&
arg.property.type === 'NumericLiteral'
)
) {
throw new InvalidRequireCallError(getInvalidProdRequireMessage(node));
}
return args[0];
}
getIndex(memberExpression) {
@ -111,6 +130,7 @@ function createMapLookup(dependencyMapIdentifier, propertyIdentifier) {
}
function collectDependencies(ast, replacement, dependencyMapIdentifier) {
const visited = new WeakSet();
const traversalState = {dependencyMapIdentifier};
traverse(
ast,
@ -124,14 +144,18 @@ function collectDependencies(ast, replacement, dependencyMapIdentifier) {
},
CallExpression(path, state) {
const node = path.node;
const arg = node.arguments[0];
if (replacement.isRequireCall(node.callee, arg)) {
if (visited.has(node)) {
return;
}
if (isRequireCall(node.callee)) {
const arg = replacement.getRequireCallArg(node);
const index = replacement.getIndex(arg);
node.arguments = replacement.makeArgs(
types.numericLiteral(index),
arg,
state.dependencyMapIdentifier,
);
visited.add(node);
}
},
},
@ -152,6 +176,16 @@ function isLiteralString(node) {
);
}
function isRequireCall(callee) {
return callee.type === 'Identifier' && callee.name === 'require';
}
class InvalidRequireCallError extends Error {
constructor(message) {
super(message);
}
}
const xp = (module.exports = (ast: AST) =>
collectDependencies(ast, new Replacement()));
@ -165,3 +199,5 @@ xp.forOptimization = (
new ProdReplacement(names),
dependencyMapName ? types.identifier(dependencyMapName) : undefined,
);
xp.InvalidRequireCallError = InvalidRequireCallError;