├── README.md ├── lib ├── entry.js ├── estoc.js ├── extract-tar.js ├── handle-external-module.js ├── handle-internal-module.js ├── module.js ├── require-impl.js ├── runtime.js ├── spy.js ├── unexecuted-list.js └── visitor.js ├── package.json └── test.js /README.md: -------------------------------------------------------------------------------- 1 | # estoc 2 | 3 | Under construction. Pardon the dust! 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /lib/entry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const estoc = require('./estoc.js') 4 | const fs = require('fs') 5 | 6 | if (require.main === module) { 7 | main(process.argv.slice(2)) 8 | } 9 | 10 | function main (argv) { 11 | if (!argv[0]) { 12 | return console.log('pass a path to the command, geez') 13 | } 14 | 15 | const maybeTarball = /\.(tar.gz|tar|tgz|t.z)$/.test(argv[0]) 16 | 17 | return (maybeTarball ? 18 | estoc.createTarballStream : 19 | estoc.createFileStream)(argv[0]) 20 | .on('data', ondata) 21 | 22 | function ondata (info) { 23 | console.log( 24 | prettyPosition(info.accessChain[info.accessChain.length - 1]) + ' ' + 25 | info.accessChain[info.accessChain.length - 1].manner + ' ' + 26 | info.accessChain.map(xs => xs.name).join('.') 27 | ) 28 | } 29 | } 30 | 31 | function prettyPosition (acc) { 32 | return [ 33 | acc.filename, 34 | acc.position.start.line, 35 | acc.position.start.column 36 | ].join(':') 37 | } 38 | -------------------------------------------------------------------------------- /lib/estoc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | createTarballStream: createTarballStream, 5 | createFileStream: createFileStream 6 | } 7 | 8 | const extractFiles = require('./extract-tar.js') 9 | const Readable = require('stream').Readable 10 | const Visitor = require('./visitor.js') 11 | 12 | function createTarballStream (filename) { 13 | const stream = new Readable({objectMode: true}) 14 | stream._read = _=>_ 15 | 16 | extractFiles(filename, onfs) 17 | return stream 18 | 19 | function onfs (err, resolveFS) { 20 | if (err) { 21 | stream.emit('error', err) 22 | return 23 | } 24 | const visitor = new Visitor(stream, resolveFS) 25 | const entryPoints = [] 26 | try { 27 | entryPoints.push.apply(entryPoints, resolveFS.getNames()) 28 | } catch (err) { 29 | } 30 | return iter() 31 | 32 | function iter () { 33 | if (!entryPoints.length) { 34 | return oncomplete() 35 | } 36 | visitor.run(entryPoints.shift(), function (err) { 37 | if (err) { 38 | return oncomplete(err) 39 | } 40 | iter() 41 | }) 42 | } 43 | } 44 | 45 | function oncomplete (err) { 46 | if (err) { 47 | return stream.emit('error', err) 48 | } 49 | stream.push(null) 50 | } 51 | } 52 | 53 | function createFileStream (filename, ready) { 54 | const stream = new Readable({objectMode: true}) 55 | const visitor = new Visitor(stream) 56 | stream._read = _=>_ 57 | visitor.run(filename, oncomplete) 58 | return stream 59 | 60 | function oncomplete (err) { 61 | if (err) { 62 | return stream.emit('error', err) 63 | } 64 | stream.push(null) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/extract-tar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = extract 4 | 5 | const concat = require('concat-stream') 6 | const tarFiles = require('tar-files') 7 | const mime = require('mime') 8 | const path = require('path') 9 | const fs = require('fs') 10 | 11 | class FSResolver { 12 | constructor (mapping, packageJSONPath) { 13 | this._mapping = mapping 14 | this._packageJSONPath = packageJSONPath 15 | } 16 | readFile (filename, enc, ready) { 17 | if (arguments.length === 2) { 18 | ready = enc 19 | enc = null 20 | } 21 | filename = filename.slice(1) 22 | var buf = this._mapping.get(filename) 23 | if (!buf) { 24 | return ready(new Error('no such file: ' + filename)) 25 | } 26 | buf = enc ? buf.toString(enc) : buf 27 | return ready(null, buf) 28 | } 29 | isFile (filename, ready) { 30 | return ready(null, this._mapping.has(filename.slice(1))) 31 | } 32 | getNames () { 33 | var buf = this._mapping.get(this._packageJSONPath) 34 | if (!buf) { 35 | throw new Error('no package.json found') 36 | } 37 | var json = {} 38 | try { 39 | json = JSON.parse(buf.toString('utf8')) 40 | } catch (err) { 41 | } 42 | var main = json.main || './index.js' 43 | var dir = '/' + this._packageJSONPath.split('/')[0] 44 | var out = [path.join(dir, main), path.join(dir, json.name + '.js')] 45 | if (json.bin) { 46 | if (typeof json.bin === 'string') { 47 | json.bin = {keyDoesntMatter: json.bin} 48 | } 49 | for (var key in json.bin) { 50 | var candidate = path.join(dir, json.bin[key]) 51 | if ((this._mapping.get(candidate.slice(1)) || [])[0] === 35) { 52 | if (/(node|iojs)\s*$/.test( 53 | this._mapping.get(candidate.slice(1)).toString('utf8').split('\n')[0] 54 | )) { 55 | out.push(candidate) 56 | } 57 | } 58 | } 59 | } 60 | return out.filter(xs => this._mapping.has(xs.slice(1))) 61 | } 62 | } 63 | 64 | function extract (filename, ready) { 65 | var output = new Map() 66 | var packageJSON = null 67 | tarFiles(filename, function (stream, ready) { 68 | if (stream.path.split('/').length === 2 && 69 | /.*\/package.json$/.test(stream.path)) { 70 | packageJSON = stream.path 71 | } 72 | 73 | // omit anything bigger than 16kb 74 | if (stream.size > 16384) { 75 | return ready() 76 | } 77 | 78 | // skip bundled deps 79 | if (/node_modules/.test(stream.path)) { 80 | return ready() 81 | } 82 | 83 | if (!/^(text|application)/.test(mime.lookup(stream.path))) { 84 | return ready() 85 | } 86 | 87 | stream.pipe(concat(function (buf) { 88 | output.set(stream.path, buf) 89 | })) 90 | }, function (err) { 91 | if (err) { 92 | return ready(err) 93 | } 94 | return ready(null, new FSResolver(output, packageJSON)) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /lib/handle-external-module.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = handleExternalModule 4 | 5 | function handleExternalModule (visitor, sourceModule, targetName) { 6 | var spy = visitor.createSpy(targetName) 7 | visitor.report(spy) 8 | return visitor.cfg._valueStack.push(spy) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /lib/handle-internal-module.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = handleInternalModule 4 | 5 | const wrapper = require('module').wrapper 6 | const esprima = require('esprima') 7 | const resolve = require('resolve') 8 | const path = require('path') 9 | 10 | const Module = require('./module.js') 11 | 12 | function handleInternalModule (visitor, sourceModule, targetName, ready) { 13 | var targetFilename = null 14 | 15 | return resolve(targetName, { 16 | basedir: path.dirname(sourceModule.filename), 17 | isFile: visitor.fs.isFile, 18 | readFile: visitor.fs.readFile 19 | }, onfilename) 20 | 21 | function onfilename (err, targetFilename_) { 22 | if (err) { 23 | return ready(err) 24 | } 25 | 26 | targetFilename = targetFilename_ 27 | if (visitor.moduleCache.has(targetFilename)) { 28 | return ready(null, visitor.moduleCache.get(targetFilename), true) 29 | } 30 | 31 | return visitor.fs.readFile(targetFilename, 'utf8', onfile) 32 | } 33 | 34 | function onfile (err, data) { 35 | if (err) { 36 | return ready(err) 37 | } 38 | 39 | if (data.slice(0, 2) === '#!') { 40 | data = data.split('\n').slice(1).join('\n') 41 | } 42 | if (path.extname(targetFilename) === '.json') { 43 | data = 'module.exports = ' + data 44 | } 45 | try { 46 | var ast = esprima.parse( 47 | wrapper.join(data), 48 | {loc: true} 49 | ) 50 | } catch (err) { 51 | // XXX: log error 52 | return ready(err) 53 | } 54 | 55 | const module = new Module( 56 | targetFilename, 57 | ast, 58 | visitor.cfg.makeObject() 59 | ) 60 | visitor.moduleCache.set(module.filename, module) 61 | module.execute(visitor) 62 | return ready(null, module) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = exportModule() 4 | 5 | const RequireImpl = require('./require-impl.js') 6 | const path = require('path') 7 | 8 | function exportModule () { 9 | var ID = 0 10 | return class Module { 11 | constructor(filename, ast, exportsObject) { 12 | this.filename = filename 13 | this.id = ID++ 14 | this.ast = ast 15 | this.exports = exportsObject 16 | this.complete = false 17 | } 18 | execute(visitor) { 19 | return executeModule(visitor, this) 20 | } 21 | } 22 | } 23 | 24 | function executeModule (visitor, module) { 25 | if (visitor.moduleCache.has(module)) { 26 | return visitor.moduleCache.get(module).exports 27 | } 28 | const moduleObject = visitor.cfg.makeObject() 29 | moduleObject.newprop('exports').assign(module.exports) 30 | visitor.moduleStack.push(module) 31 | visitor.cfg.insertFrame(pauseOnModuleWrapper) 32 | 33 | const currentScope = visitor.cfg.resetScope() 34 | return visitor.cfg._visit(module.ast) 35 | 36 | function pauseOnModuleWrapper () { 37 | visitor.cfg.resetScope(currentScope) 38 | const filenameValue = visitor.cfg.makeValue( 39 | 'string', 40 | module.filename 41 | ) 42 | const dirnameValue = visitor.cfg.makeValue( 43 | 'string', 44 | path.dirname(module.filename) 45 | ) 46 | 47 | visitor.moduleCache.set(module, module.exports) 48 | // re-set the breakpoint 49 | visitor.cfg.insertFrame(pauseOnExecutedModule) 50 | visitor.unexecutedFunctions.delete(visitor.lastFunction) 51 | visitor.lastFunction.module = module 52 | visitor.lastFunction.call(visitor.cfg, visitor.cfg.global(), [ 53 | module.exports, 54 | getRequireFor(visitor, module), 55 | moduleObject, 56 | filenameValue, 57 | dirnameValue 58 | ]) 59 | 60 | // resume execution! 61 | return true 62 | } 63 | 64 | function pauseOnExecutedModule () { 65 | visitor.moduleStack.pop() 66 | module.exports = moduleObject.getprop('exports').value() 67 | 68 | if (module.exports.sharedFunctionInfo && 69 | module.exports.sharedFunctionInfo()) { 70 | module.exports.sharedFunctionInfo().isExport = true 71 | } 72 | if (module.exports.names) { 73 | for (var name of module.exports.names()) { 74 | if (name.value() && 75 | name.value().sharedFunctionInfo && 76 | name.value().sharedFunctionInfo()) { 77 | name.value().sharedFunctionInfo().isExport = true 78 | } 79 | } 80 | } 81 | visitor.cfg._valueStack.pop() 82 | visitor.cfg._valueStack.push(module.exports) 83 | module.complete = true 84 | return true 85 | } 86 | } 87 | 88 | function getRequireFor (visitor, module) { 89 | var fn = visitor.cfg.makeFunction(function (cfg, ctxt, args, isNew) { 90 | return RequireImpl(visitor, module, cfg, ctxt, args, isNew) 91 | }) 92 | fn.visitor = visitor 93 | fn.module = module 94 | return fn 95 | } 96 | -------------------------------------------------------------------------------- /lib/require-impl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = RequireImpl 4 | 5 | const handleExternalModule = require('./handle-external-module.js') 6 | const handleInternalModule = require('./handle-internal-module.js') 7 | 8 | function RequireImpl (visitor, module, cfg, ctxt, args, isNew) { 9 | var target = args[0] 10 | 11 | if (!target) { 12 | return cfg._valueStack.push(cfg.makeUnknown()) 13 | } 14 | 15 | if (target.isEither()) { 16 | // we should return an Either<> of the result 17 | // of calling RequireImpl with each outcome. 18 | var outcomes = target.outcomes() 19 | var results = [] 20 | return enumerateRequireOutcomes(outcomes, this, visitor, module, results) 21 | } 22 | 23 | if (!target.isString() || !target._value) { 24 | return cfg._valueStack.push(cfg.makeUnknown()) 25 | } 26 | 27 | if (!/^(\w:\\|\\|\/|\.)/.test(target._value)) { 28 | return handleExternalModule(visitor, module, target._value) 29 | } 30 | 31 | visitor.paused = true 32 | var sync = true 33 | handleInternalModule(visitor, module, target._value, onInternal) 34 | sync = false 35 | return 36 | 37 | function onInternal (err, module, fromCache) { 38 | if (err) { 39 | cfg._valueStack.push(cfg.makeUnknown()) 40 | } else if (fromCache) { 41 | cfg._valueStack.push(module.exports) 42 | } 43 | visitor.paused = false 44 | if (!sync) { 45 | visitor.onresume() 46 | } 47 | } 48 | } 49 | 50 | function enumerateRequireOutcomes (outcomeIter, fnObj, visitor, module, results) { 51 | var next = outcomeIter.next() 52 | if (next.done) { 53 | return visitor.cfg._valueStack.push( 54 | visitor.cfg.makeEither(results) 55 | ) 56 | } 57 | 58 | visitor.cfg.insertFrame(function () { 59 | results.push(visitor.cfg._valueStack.pop()) 60 | enumerateRequireOutcomes(outcomeIter, fnObj, visitor, module, results) 61 | return true 62 | }) 63 | RequireImpl.call(fnObj, visitor, module, visitor.cfg, null, [next.value], false) 64 | } 65 | -------------------------------------------------------------------------------- /lib/runtime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = define 4 | 5 | function define(cfg, createSpy) { 6 | var global = cfg.global() 7 | global.newprop('global').assign(global) 8 | 9 | var globals = [ 10 | 'Buffer', 11 | 'setTimeout', 12 | 'setInterval', 13 | 'clearTimeout', 14 | 'clearInterval', 15 | 'setImmediate', 16 | 'clearImmediate', 17 | 'process', 18 | 'root', 19 | 'GLOBAL', 20 | 'console' 21 | ] 22 | 23 | globals.forEach(function(globalName) { 24 | global.newprop(globalName).assign(createSpy(globalName)) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /lib/spy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = Spy 4 | 5 | const ObjectValue = require('escontrol/lib/values/object') 6 | const inherits = require('inherits') 7 | 8 | function Spy (parent, access, visitor, parentMap) { 9 | ObjectValue.call( 10 | this, 11 | visitor.cfg, 12 | -1, 13 | visitor.cfg._builtins.getprop('[[FunctionProto]]').value(), 14 | parentMap 15 | ) 16 | this.access = access 17 | this.parent = parent 18 | this.visitor = visitor 19 | } 20 | 21 | inherits(Spy, ObjectValue) 22 | 23 | const proto = Spy.prototype 24 | 25 | Spy.REQUIRE = 'require' 26 | Spy.IOCARG = 'ioc-arg' 27 | Spy.OBJECTLOAD = 'object' 28 | Spy.CONTEXTLOAD = 'context' 29 | Spy.RETURNVAL = 'return-value' 30 | Spy.INSTANT = 'instantiate' 31 | Spy.OBJECTDELETE = 'delete' 32 | 33 | Spy.createRoot = function (visitor, name) { 34 | return new Spy(null, makeAccess(visitor, Spy.REQUIRE, name), visitor) 35 | } 36 | 37 | proto.cloneLoad = function (name) { 38 | var spy = new Spy(this, makeAccess(this.visitor, Spy.CONTEXTLOAD, name), this.visitor) 39 | this.visitor.report(spy) 40 | return spy 41 | } 42 | 43 | proto.getprop = function (prop, immediate) { 44 | var name = ObjectValue.prototype.getprop.call(this, prop, immediate) 45 | if (!name) { 46 | name = this.newprop(prop) 47 | var spy = new Spy( 48 | this, 49 | makeAccess(this.visitor, Spy.OBJECTLOAD, prop), 50 | this.visitor 51 | ) 52 | this.visitor.report(spy) 53 | name.assign(spy) 54 | } 55 | 56 | return name 57 | } 58 | 59 | proto.delprop = function (propname) { 60 | var retVal = ObjectValue.prototype.delprop.call(this, propname) 61 | var spy = new Spy( 62 | this, 63 | makeAccess(this.visitor, Spy.OBJECTDELETE, propname), 64 | this.visitor 65 | ) 66 | this.visitor.report(spy) 67 | return retVal 68 | } 69 | 70 | proto.copy = function () { 71 | return new Spy( 72 | this, 73 | this.access, 74 | this.visitor, 75 | this._attributes 76 | ) 77 | } 78 | 79 | proto.isFunction = function () { 80 | return true 81 | } 82 | 83 | proto.isSpy = true 84 | 85 | proto.classInfo = function () { 86 | return '' 87 | } 88 | 89 | proto.instantiate = function (cfg, args) { 90 | var spy = new Spy( 91 | this, 92 | makeAccess(this.visitor, Spy.INSTANT, ''), 93 | this.visitor 94 | ) 95 | this.visitor.report(spy, args) 96 | for (var i = 0; i < args.length; ++i) { 97 | if (args[i].isFunction()) { 98 | args[i].setMark('ioc', this) 99 | } 100 | } 101 | this.visitor.cfg._valueStack.push(spy) 102 | } 103 | 104 | proto.call = function (cfg, ctxt, args, isNew, branch) { 105 | var spy = new Spy( 106 | this, 107 | makeAccess(this.visitor, Spy.RETURNVAL, ''), 108 | this.visitor 109 | ) 110 | this.visitor.report(spy, args) 111 | for (var i = 0; i < args.length; ++i) { 112 | if (args[i].isFunction()) { 113 | args[i].setMark('ioc', this) 114 | } 115 | } 116 | this.visitor.cfg._valueStack.push(spy) 117 | } 118 | 119 | proto.makeIOCArgument = function (name, idx) { 120 | var spy = new Spy(this, { 121 | manner: Spy.IOCARG, 122 | filename: this.access.filename, 123 | position: this.access.position, 124 | name: '' 125 | }, this.visitor) 126 | this.visitor.report(spy) 127 | return spy 128 | } 129 | 130 | function makeAccess (visitor, manner, name) { 131 | var lastFn = visitor.cfg._callStack.current().getFunction() 132 | var lastAST = visitor.cfg.lastASTNode() || {loc: {start: 0, column: 0}} 133 | var module = (lastFn ? lastFn.module : null) || {filename: ''} 134 | return { 135 | manner: manner, 136 | filename: module.filename, 137 | position: lastAST.loc || {start: {line: 0, column: 0}}, 138 | name: name 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/unexecuted-list.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = class UnexecutedList { 4 | constructor () { 5 | this._map = new Map() 6 | this._seen = new WeakSet() 7 | this._generation = 0 8 | } 9 | next () { 10 | ++this._generation 11 | if (!this._map.size) { 12 | return null 13 | } 14 | var weights = new Map() 15 | var out = [] 16 | for (var pair of this._map) { 17 | weights.set(pair[1], getWeight(pair[0], pair[1], this._generation)) 18 | out.push(pair[1]) 19 | } 20 | var fn = out.sort(function (lhs, rhs) { 21 | lhs = weights.get(lhs) 22 | rhs = weights.get(rhs) 23 | return lhs < rhs ? 1 : 24 | lhs > rhs ? -1 : 0 25 | })[0] 26 | 27 | this.delete(fn) 28 | return fn 29 | } 30 | add (fn) { 31 | if (this._seen.has(fn.sharedFunctionInfo())) { 32 | return 33 | } 34 | this._seen.add(fn.sharedFunctionInfo()) 35 | fn.sharedFunctionInfo()._generation = this._generation 36 | this._map.set(fn.sharedFunctionInfo(), fn) 37 | } 38 | delete (fn) { 39 | this._map.delete(fn.sharedFunctionInfo()) 40 | } 41 | } 42 | 43 | function getWeight (sfi, fn, gen) { 44 | var size = Math.min(sfi._node.loc.end.line - sfi._node.loc.start.line, 500) / 500 45 | var numRefs = Math.min(fn._references.size, 100) / 100 46 | var arity = 10 - Math.min(sfi.arity(), 10) / 10 47 | var generation = (100 - Math.min(gen - sfi._generation, 100)) / 100 48 | return -fn.module.id * 100 + 49 | Number(Boolean(sfi.isExport)) * 10 + 50 | size * 0.75 + 51 | generation * 0.25 52 | } 53 | -------------------------------------------------------------------------------- /lib/visitor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = exportVisitor() 4 | 5 | const escontrol = require('escontrol') 6 | const path = require('path') 7 | const fs = require('fs') 8 | 9 | const handleInternalModule = require('./handle-internal-module.js') 10 | const UnexecutedList = require('./unexecuted-list.js') 11 | const RequireImpl = require('./require-impl.js') 12 | const defineRuntime = require('./runtime.js') 13 | const Spy = require('./spy.js') 14 | 15 | function defaultIsFile (file, ready) { 16 | fs.stat(file, function (err, stat) { 17 | if (err && err.code === 'ENOENT') return ready(null, false) 18 | if (err) return ready(err) 19 | return ready(null, stat.isFile()) 20 | }) 21 | } 22 | 23 | function exportVisitor() { 24 | return class Visitor { 25 | constructor (stream, userFS) { 26 | this.fs = userFS || { 27 | readFile: fs.readFile, 28 | isFile: defaultIsFile 29 | } 30 | this.fs.readFile = this.fs.readFile.bind(this.fs) 31 | this.fs.isFile = this.fs.isFile.bind(this.fs) 32 | this.stream = stream 33 | this.paused = false 34 | this.onresume = null 35 | this.moduleStack = [] 36 | this.moduleCache = new Map() 37 | this.lastFunction = null 38 | this.unexecutedFunctions = new UnexecutedList() 39 | this.cfg = escontrol(createEmptyProgramAST(), { 40 | onfunction: (fn, ast) => { 41 | return onfunction(this, fn, ast) 42 | }, 43 | onload: (name, value, node) => { 44 | return onload(this, name, value, node) 45 | }, 46 | oncall: (fn, ctxt, args, recr) => { 47 | return oncall(this, fn, ctxt, args, recr) 48 | } 49 | }) 50 | defineRuntime(this.cfg, name => this.createSpy(name)) 51 | } 52 | 53 | currentFileName () { 54 | return this.moduleStack[this.moduleStack.length - 1].filename 55 | } 56 | 57 | run (filename, ready) { 58 | handleInternalModule(this, { 59 | filename: '/dne.js' 60 | }, filename, (err, module) => { 61 | if (err) { 62 | return ready(err) 63 | } 64 | if (module.complete) { 65 | return ready() 66 | } 67 | advance(this, ready) 68 | }) 69 | } 70 | 71 | createSpy (name) { 72 | return Spy.createRoot(this, name) 73 | } 74 | 75 | report (spy, args) { 76 | var current = spy 77 | var acc = [] 78 | while (current) { 79 | acc.unshift(current.access) 80 | current = current.parent 81 | } 82 | this.stream.push({ 83 | accessChain: acc, 84 | args: args, 85 | handled: hasHandler(this.cfg) 86 | }) 87 | } 88 | } 89 | } 90 | 91 | function hasHandler (cfg) { 92 | var result = false 93 | try { 94 | result = cfg.getExceptionDestination() !== null 95 | } finally { 96 | return result 97 | } 98 | } 99 | 100 | function advance (visitor, ready) { 101 | const cfg = visitor.cfg 102 | visitor.onresume = iter 103 | return iter() 104 | 105 | function run() { 106 | for (var i = 0; i < 1000; ++i) { 107 | if (visitor.paused) { 108 | return 109 | } 110 | if (!cfg.advance()) { 111 | if (!iterateUnexecuted(visitor)) { 112 | return ready() 113 | } 114 | } 115 | } 116 | if (i === 1000) { 117 | return setImmediate(iter) 118 | } 119 | } 120 | 121 | function iter() { 122 | try { 123 | run() 124 | } catch (err) { 125 | return ready(err) 126 | } 127 | } 128 | } 129 | 130 | function onfunction (visitor, fn, ast) { 131 | visitor.lastFunction = fn 132 | visitor.unexecutedFunctions.add(fn) 133 | 134 | var frame = visitor.cfg._callStack.current() 135 | if (frame) { 136 | visitor.stream.emit('defn', fn, frame.getFunction()) 137 | } 138 | var caller = visitor.cfg._callStack.current().getFunction() 139 | fn.module = caller && caller.module ? caller.module : 140 | visitor.moduleStack[visitor.moduleStack.length - 1] 141 | } 142 | 143 | function oncall (visitor, fn, ctxt, args, isRecursion) { 144 | if (fn.isEither()) { 145 | // only skip the call if all of the outcomes have been visited. 146 | var shouldCall = false 147 | for (const xs of fn.outcomes()) { 148 | if (xs.isFunction() && !xs.isUnknown()) { 149 | if (oncall(visitor, xs, ctxt, args, isRecursion)) { 150 | // NB: can't bail early, we want to make sure 151 | // each fn gets removed. 152 | shouldCall = true 153 | } 154 | } 155 | } 156 | return shouldCall 157 | } 158 | 159 | if (visitor.cfg._callStack.current()) { 160 | visitor.stream.emit( 161 | 'callfn', 162 | visitor.cfg._callStack.current().getFunction(), 163 | fn 164 | ) 165 | } 166 | // only (naturally) execute any given function once. 167 | if (fn.isFunction() && 168 | !fn.isUnknown() && 169 | fn.sharedFunctionInfo) { 170 | visitor.unexecutedFunctions.delete(fn) 171 | const sfi = fn.sharedFunctionInfo() 172 | if (sfi) { 173 | if (fn.call === RequireImpl) { 174 | return true 175 | } 176 | if (sfi._fakeCall) { 177 | return sfi.callCount() < 2 178 | } 179 | return sfi.callCount() < 1 180 | } 181 | } 182 | } 183 | 184 | function createEmptyProgramAST () { 185 | return { 186 | 'type': 'Program', 187 | 'body': [] 188 | } 189 | } 190 | 191 | function onload (visitor, name, value, node) { 192 | if (value.isSpy) { 193 | visitor.cfg._valueStack.pop() 194 | visitor.cfg._valueStack.push(value.cloneLoad(node.name)) 195 | } 196 | } 197 | 198 | function iterateUnexecuted (visitor) { 199 | const func = visitor.unexecutedFunctions.next() 200 | if (!func) { 201 | return 202 | } 203 | const sfi = func.sharedFunctionInfo() 204 | const arity = sfi.arity() 205 | const args = new Array(arity) 206 | const argNames = sfi.parameters() 207 | sfi._fakeCall = true 208 | const marks = func.getMark('ioc') 209 | if (marks.length) { 210 | const spy = marks[0] 211 | for (var i = 0; i < arity; ++i) { 212 | args[i] = spy.makeIOCArgument(argNames[i], i) 213 | } 214 | visitor.stream.emit('ioc-callfn', spy, func) 215 | } else { 216 | for (var i = 0; i < arity; ++i) { 217 | args[i] = visitor.cfg.makeUnknown() 218 | } 219 | } 220 | 221 | var sourceFunction = null 222 | for (var xs of func._references) { 223 | var source = xs.getCurrentSourceObject() 224 | 225 | // skip the function's own prototype object 226 | // (fn.prototype.constructor === fn) 227 | if (source === func.getprop('prototype').value()) { 228 | continue 229 | } 230 | 231 | for (var xs of source._references) { 232 | if (xs._name === 'prototype') { 233 | sourceFunction = xs.getCurrentSourceObject() 234 | break 235 | } 236 | } 237 | 238 | if (sourceFunction) { 239 | break 240 | } 241 | } 242 | var context = sourceFunction && sourceFunction.makeNew ? 243 | sourceFunction.makeNew() : 244 | visitor.cfg.makeUnknown() 245 | 246 | visitor.cfg.insertFrame(oncomplete) 247 | visitor.moduleStack.push(func.module) 248 | func.call(visitor.cfg, context, args) 249 | return true 250 | 251 | function oncomplete () { 252 | visitor.moduleStack.pop() 253 | visitor.cfg._valueStack.pop() 254 | return true 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estoc", 3 | "version": "3.1.4", 4 | "description": "a static analyzer for npm packages", 5 | "main": "./lib/estoc.js", 6 | "bin": { 7 | "estoc": "./lib/entry.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Chris Dickinson (http://neversaw.us/)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "concat-stream": "^1.5.0", 16 | "escontrol": "^6.1.3", 17 | "espree": "^2.0.0", 18 | "esprima": "^2.5.0", 19 | "inherits": "^2.0.1", 20 | "mime": "^1.3.4", 21 | "once": "^1.3.1", 22 | "resolve": "^1.1.6", 23 | "tar-files": "^1.0.2", 24 | "tar-stream": "^1.1.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env iojs 2 | const createUsageStream = require('./index.js') 3 | 4 | var stream = require('stream') 5 | var Transform = stream.Transform 6 | 7 | var txf = new Transform({ 8 | objectMode: true, 9 | transform: function(usage, _, ready) { 10 | ready(null, new Buffer(String(usage.info() || '') + '\n')) 11 | } 12 | }) 13 | 14 | createUsageStream(process.argv[2]).pipe(txf).pipe(process.stdout) 15 | --------------------------------------------------------------------------------