Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions find-fast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var acorn = require('acorn');
var defined = require('defined');

var ST_NONE = 0;
var ST_SAW_NAME = 1;
var ST_INSIDE_CALL = 2;
var ST_MEMBER_EXPRESSION = 3;

module.exports = function findFast(src, opts) {
if (!opts) opts = {};
if (typeof src !== 'string') src = String(src);
if (opts.word === undefined) opts.word = 'require';

var tokenizer = acorn.tokenizer(src, opts.parse);
var token;
var state = ST_NONE;

var opener;
var args = [];

var modules = { strings: [], expressions: [] };
if (opts.nodes) modules.nodes = [];

while ((token = tokenizer.getToken()) && token.type !== acorn.tokTypes.eof) {
if (state !== ST_INSIDE_CALL && token.type === acorn.tokTypes.dot) {
state = ST_MEMBER_EXPRESSION;
} else if (state === ST_NONE && token.type === acorn.tokTypes.name && mayBeRequire(token)) {
state = ST_SAW_NAME;
opener = token;
} else if (state === ST_SAW_NAME && token.type === acorn.tokTypes.parenL) {
state = ST_INSIDE_CALL;
args = [];
} else if (state === ST_INSIDE_CALL) {
if (token.type === acorn.tokTypes.parenR) { // End of fn() call
if (args.length === 1 && args[0].type === acorn.tokTypes.string) {
modules.strings.push(args[0].value);
} else if (args.length === 3 // A template string without any expressions
&& args[0].type === acorn.tokTypes.backQuote
&& args[1].type === acorn.tokTypes.template
&& args[2].type === acorn.tokTypes.backQuote) {
modules.strings.push(args[1].value);
} else if (args.length > 0) {
modules.expressions.push(src.slice(args[0].start, args[args.length - 1].end));
}

if (opts.nodes) {
// Cut `src` at the end of this call, so that parseExpressionAt doesn't consider the `.abc` in
// `require('xyz').abc`
var chunk = src.slice(0, token.end);
var node = acorn.parseExpressionAt(chunk, opener.start, opts.parse);
modules.nodes.push(node);
}

state = ST_NONE;
} else {
args.push(token);
}
} else {
state = ST_NONE;
}
}
return modules;

function mayBeRequire(token) {
return token.type === acorn.tokTypes.name &&
token.value === opts.word;
}
}
31 changes: 26 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
var acorn = require('acorn-node');
var walk = require('acorn-node/walk');
var copy = require('shallow-copy');
var defined = require('defined');
var fastFind = require('./find-fast');

var requireRe = /\brequire\b/;

function parse (src, opts) {
if (!opts) opts = {};
return acorn.parse(src, {
function getParseOpts (opts) {
opts = opts || {};
return {
ecmaVersion: defined(opts.ecmaVersion, 9),
sourceType: defined(opts.sourceType, 'script'),
ranges: defined(opts.ranges, opts.range),
Expand All @@ -19,7 +21,7 @@ function parse (src, opts) {
opts.allowImportExportEverywhere, true
),
allowHashBang: defined(opts.allowHashBang, true)
});
};
}

var exports = module.exports = function (src, opts) {
Expand All @@ -28,6 +30,12 @@ var exports = module.exports = function (src, opts) {

exports.find = function (src, opts) {
if (!opts) opts = {};
else opts = copy(opts);
opts.parse = getParseOpts(opts.parse);

if (!opts.isRequire && !opts.fullParse) {
return fastFind(src, opts);
}

var word = opts.word === undefined ? 'require' : opts.word;
if (typeof src !== 'string') src = String(src);
Expand All @@ -44,11 +52,12 @@ exports.find = function (src, opts) {
var wordRe = word === 'require' ? requireRe : RegExp('\\b' + word + '\\b');
if (!wordRe.test(src)) return modules;

var ast = parse(src, opts.parse);
var ast = acorn.parse(src, opts.parse);

function visit(node, st, c) {
var hasRequire = wordRe.test(src.slice(node.start, node.end));
if (!hasRequire) return;
if (redefinesRequire(node)) return;
walk.base[node.type](node, st, c);
if (node.type !== 'CallExpression') return;
if (isRequire(node)) {
Expand All @@ -75,6 +84,18 @@ exports.find = function (src, opts) {
Statement: visit,
Expression: visit
});

// Detect `require` redefinitions in function parameter lists, like
// in `[function(require,module,exports){` generated by browser-pack.
// This is a simple way to address the 99% case without doing full scope analysis
function redefinesRequire(node) {
if (node.type === 'FunctionExpression') {
return node.params.some(function (param) {
return param.type === 'Identifier' && param.name === word;
});
}
return false;
}

return modules;
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"acorn-node": "^1.3.0",
"defined": "^1.0.0",
"minimist": "^1.1.1"
"shallow-copy": "0.0.1"
},
"devDependencies": {
"tap": "^10.7.3"
Expand Down
11 changes: 11 additions & 0 deletions test/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var test = require('tap').test;
var detective = require('../');
var fs = require('fs');
var src = fs.readFileSync(__dirname + '/files/comment.js');

test('comment', function (t) {
var modules = detective.find(src);
t.deepEqual(modules.strings, [ 'beep' ]);
t.notOk(modules.nodes, 'has no nodes');
t.end();
});
5 changes: 5 additions & 0 deletions test/files/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var x = require /* idk */
// whatever
(
'beep' // boop
)
8 changes: 8 additions & 0 deletions test/files/scope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
(function(modules){
modules[1](function(i){return modules[i]()})
})({1:[function (require,module,exports) {
require('./y')
},{'./y':2}],2:function(require,module,exports){
console.log("abc")
}})
require('./z')
9 changes: 9 additions & 0 deletions test/scope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
var test = require('tap').test;
var detective = require('../');
var fs = require('fs');
var src = fs.readFileSync(__dirname + '/files/scope.js');

test('scope', function (t) {
t.plan(1);
t.deepEqual(detective(src), [ './z' ]);
});