diff --git a/find-fast.js b/find-fast.js new file mode 100644 index 000000000..b0465d51e --- /dev/null +++ b/find-fast.js @@ -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; + } +} diff --git a/index.js b/index.js index 382d701a9..fdd65b512 100644 --- a/index.js +++ b/index.js @@ -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), @@ -19,7 +21,7 @@ function parse (src, opts) { opts.allowImportExportEverywhere, true ), allowHashBang: defined(opts.allowHashBang, true) - }); + }; } var exports = module.exports = function (src, opts) { @@ -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); @@ -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)) { @@ -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; }; diff --git a/package.json b/package.json index 610bbd0bd..bfed9f14c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/comment.js b/test/comment.js new file mode 100644 index 000000000..985ec5d61 --- /dev/null +++ b/test/comment.js @@ -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(); +}); diff --git a/test/files/comment.js b/test/files/comment.js new file mode 100644 index 000000000..7664ebeb0 --- /dev/null +++ b/test/files/comment.js @@ -0,0 +1,5 @@ +var x = require /* idk */ +// whatever +( +'beep' // boop +) diff --git a/test/files/scope.js b/test/files/scope.js new file mode 100644 index 000000000..66ca0aed5 --- /dev/null +++ b/test/files/scope.js @@ -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') diff --git a/test/scope.js b/test/scope.js new file mode 100644 index 000000000..e6b29bbb9 --- /dev/null +++ b/test/scope.js @@ -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' ]); +});