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
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,35 +101,88 @@ it('collects mixed dependencies as being sync; reverse order', () => {
);
});
it('supports template literals as arguments', () => {
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\`);`),
);
});
describe('Evaluating static arguments', () => {
it('supports template literals as arguments', () => {
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 on template literals with interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)');
try {
collectDependencies(ast);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
expect(error.message).toMatchSnapshot();
}
});
it('supports template literals with static interpolations', () => {
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 on tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
try {
collectDependencies(ast);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
expect(error.message).toMatchSnapshot();
}
it('throws template literals with dyncamic interpolations', () => {
const ast = astFromCode('let foo;require(`left${foo}pad`)');
try {
collectDependencies(ast);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
expect(error.message).toMatchSnapshot();
}
});
it('throws on tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
try {
collectDependencies(ast);
throw new Error('should not reach');
} catch (error) {
expect(error).toBeInstanceOf(InvalidRequireCallError);
expect(error.message).toMatchSnapshot();
}
});
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', () => {

View File

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