Make collectDependencies smarter when evaluating the require argument

Reviewed By: BYK

Differential Revision: D6592104

fbshipit-source-id: d90a93c51cb04e38cc172eb8eda932f64fce0075
This commit is contained in:
Rafael Oleza 2017-12-18 12:29:38 -08:00 committed by Facebook Github Bot
parent 57cfa19518
commit 3b497585f1
3 changed files with 104 additions and 46 deletions

View File

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`throws on tagged template literals 1`] = `"Calls to require() expect exactly 1 string literal argument, but this was found: \`require(tag\`left-pad\`)\`."`; exports[`Evaluating static arguments throws on tagged template literals 1`] = `"Calls to require() expect exactly 1 string literal argument, but this was found: \`require(tag\`left-pad\`)\`."`;
exports[`throws on template literals with interpolations 1`] = `"Calls to require() expect exactly 1 string literal argument, but this was found: \`require(\`left\${\\"-\\"}pad\`)\`."`; exports[`Evaluating static arguments throws template literals with dyncamic interpolations 1`] = `"Calls to require() expect exactly 1 string literal argument, but this was found: \`require(\`left\${foo}pad\`)\`."`;
exports[`Evaluating static arguments throws when requiring non-strings 1`] = `"Calls to require() expect exactly 1 string literal argument, but this was found: \`require(1)\`."`;

View File

@ -101,6 +101,7 @@ it('collects mixed dependencies as being sync; reverse order', () => {
); );
}); });
describe('Evaluating static arguments', () => {
it('supports template literals as arguments', () => { it('supports template literals as arguments', () => {
const ast = astFromCode('require(`left-pad`)'); const ast = astFromCode('require(`left-pad`)');
const {dependencies, dependencyMapName} = collectDependencies(ast); const {dependencies, dependencyMapName} = collectDependencies(ast);
@ -110,8 +111,17 @@ it('supports template literals as arguments', () => {
); );
}); });
it('throws on template literals with interpolations', () => { it('supports template literals with static interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)'); const ast = astFromCode('require(`left${"-"}pad`)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
expect(dependencies).toEqual([{name: 'left-pad', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`require(${dependencyMapName}[0], \`left\${"-"}pad\`);`),
);
});
it('throws template literals with dyncamic interpolations', () => {
const ast = astFromCode('let foo;require(`left${foo}pad`)');
try { try {
collectDependencies(ast); collectDependencies(ast);
throw new Error('should not reach'); throw new Error('should not reach');
@ -132,6 +142,49 @@ it('throws on tagged template literals', () => {
} }
}); });
it('supports multiple static strings concatenated', () => {
const ast = astFromCode('require("foo_" + "bar")');
const {dependencies, dependencyMapName} = collectDependencies(ast);
expect(dependencies).toEqual([{name: 'foo_bar', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(`require(${dependencyMapName}[0], "foo_" + "bar");`),
);
});
it('supports concatenating strings and template literasl', () => {
const ast = astFromCode('require("foo_" + "bar" + `_baz`)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
expect(dependencies).toEqual([{name: 'foo_bar_baz', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(
`require(${dependencyMapName}[0], "foo_" + "bar" + \`_baz\`);`,
),
);
});
it('supports using static variables in require statements', () => {
const ast = astFromCode('const myVar="my";require("foo_" + myVar)');
const {dependencies, dependencyMapName} = collectDependencies(ast);
expect(dependencies).toEqual([{name: 'foo_my', isAsync: false}]);
expect(codeFromAst(ast)).toEqual(
comparableCode(
`const myVar = \"my\"; require(${dependencyMapName}[0], "foo_" + myVar);`,
),
);
});
it('throws when requiring non-strings', () => {
const ast = astFromCode('require(1)');
try {
collectDependencies(ast);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
expect(error.message).toMatchSnapshot();
}
});
});
it('exposes a string as `dependencyMapName` even without collecting dependencies', () => { it('exposes a string as `dependencyMapName` even without collecting dependencies', () => {
const ast = astFromCode(''); const ast = astFromCode('');
expect(collectDependencies(ast).dependencyMapName).toEqual(any(String)); expect(collectDependencies(ast).dependencyMapName).toEqual(any(String));

View File

@ -58,7 +58,7 @@ function collectDependencies(ast: Ast): CollectedDependencies {
return; return;
} }
if (isRequireCall(node.callee)) { if (isRequireCall(node.callee)) {
const reqNode = processRequireCall(context, node, depMapIdent); const reqNode = processRequireCall(context, path, node, depMapIdent);
visited.add(reqNode); visited.add(reqNode);
} }
}, },
@ -76,7 +76,7 @@ function isRequireCall(callee) {
} }
function processImportCall(context, path, node, depMapIdent) { function processImportCall(context, path, node, depMapIdent) {
const [, name] = getModuleNameFromCallArgs('import', node); const [, name] = getModuleNameFromCallArgs('import', node, path);
const index = assignDependencyIndex(context, name, 'import'); const index = assignDependencyIndex(context, name, 'import');
const mapLookup = createDepMapLookup(depMapIdent, index); const mapLookup = createDepMapLookup(depMapIdent, index);
const newImport = makeAsyncRequire({ const newImport = makeAsyncRequire({
@ -86,11 +86,15 @@ function processImportCall(context, path, node, depMapIdent) {
path.replaceWith(newImport); path.replaceWith(newImport);
} }
function processRequireCall(context, node, depMapIdent) { function processRequireCall(context, path, node, depMapIdent) {
const [nameLiteral, name] = getModuleNameFromCallArgs('require', node); const [nameExpression, name] = getModuleNameFromCallArgs(
'require',
node,
path,
);
const index = assignDependencyIndex(context, name, 'require'); const index = assignDependencyIndex(context, name, 'require');
const mapLookup = createDepMapLookup(depMapIdent, index); const mapLookup = createDepMapLookup(depMapIdent, index);
node.arguments = [mapLookup, nameLiteral]; node.arguments = [mapLookup, nameExpression];
return node; return node;
} }
@ -98,21 +102,20 @@ function processRequireCall(context, node, depMapIdent) {
* Extract the module name from `require` arguments. We support template * Extract the module name from `require` arguments. We support template
* literal, for example one could write `require(`foo`)`. * literal, for example one could write `require(`foo`)`.
*/ */
function getModuleNameFromCallArgs(type, node) { function getModuleNameFromCallArgs(type, node, path) {
const args = node.arguments; if (node.arguments.length !== 1) {
if (args.length !== 1) {
throw invalidRequireOf(type, node); throw invalidRequireOf(type, node);
} }
const nameLiteral = args[0];
if (nameLiteral.type === 'StringLiteral') { const nameExpression = node.arguments[0];
return [nameLiteral, nameLiteral.value];
} // Try to evaluate the first argument of the require() statement.
if (nameLiteral.type === 'TemplateLiteral') { // If it can be statically evaluated, resolve it.
if (nameLiteral.quasis.length !== 1) { const result = path.get('arguments.0').evaluate();
throw invalidRequireOf(type, node); if (result.confident && typeof result.value === 'string') {
} return [nameExpression, result.value];
return [nameLiteral, nameLiteral.quasis[0].value.cooked];
} }
throw invalidRequireOf(type, node); throw invalidRequireOf(type, node);
} }