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

View File

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