如何在Node.js中检测函数的所有依赖项?

时间:2013-07-28 05:21:19

标签: javascript node.js

我试图全面了解我的问题。我需要编写一个Node.js程序,它应该能够检测函数的所有依赖项。

E.g。

function a() {
   //do something
   b();
};

function b() {
  console.log("Hey, This is b");
};

在上面的示例中,我需要一个像这样的JSON:

{
    "a": {
        dependencies: ["b"],
        range: [1, 4]
    },
    "b": {
        dependencies: [],
        range: [5, 8]
    }
}

dependencies属性中,我需要在函数内部调用一系列函数,而range是指函数定义的行范围。

我需要一个解决方案来实现这一目标。是否有Node.js的工具或插件?

4 个答案:

答案 0 :(得分:47)

(我提前道歉:我经常试着让我的答案变得幽默,以便通过它们让读者感到轻松,但在这种情况下,我无法成功地做到这一点。请考虑对此答案的长度采取双重道歉。 )

0。 TL; DR(对于#34;普通人")问题

这不是一个容易的问题。我们不会完全解决它,而是限制其范围 - 我们只会解决我们关心的问题部分。我们将通过使用JavaScript解析器解析输入并使用简单的recurive-descent算法对其进行检查来实现。我们的算法将分析程序的范围并正确识别函数调用。

其余的只是填补空白!结果位于答案的底部,因此如果您不想通读,我建议您抓住first comment

1。限制问题

正如Benjamin Gruenbaum's answer所说,由于JavaScript的动态特性,这是一个非常非常困难的问题。但是,如果不是制定一个适用于100%程序的解决方案,我们会为一部分程序做这件事,如果我们限制自己来处理某些事情呢?

最重要的限制:

  • eval 。如果我们加入eval,那就会陷入混乱。这是因为eval允许您使用任意字符串,这使得跟踪依赖性成为不可能,而无需检查每个可能的输入。在NodeJS中没有document.writesetTimeout只接受一个功能,所以我们不必担心这些。但是,我们也不允许使用vm module

以下限制是为了简化流程。它们可能是可以解决的,但是解决这些问题超出了这个答案的范围:

  1. 没有动态密钥 obj[key]()让我很难介绍这个限制,但在某些情况下它肯定是可以解决的(例如key = 'foo'但不是{{1} })
  2. 变量不是阴影,没有key = userInput()。绝对可以使用完整的范围解析器解决。
  3. 没有时髦的表达,例如var self = this
  4. 最后,在这个答案中对实现的限制 - 要么是因为复杂性约束,要么是时间限制(但它们是非常容易解决的):

    1. 没有提升,因此功能声明不会在范围内爆炸。
    2. 无对象处理。这很糟糕,但处理诸如(a, b)()foo.bar()之类的事情至少会使程序复杂性翻倍。投入足够的时间,这是非常可行的。
    3. 仅限功能范围。 JavaScript中有一些方法来定义除函数之外的范围(this.foo()语句,with块)。我们没有处理它们。
    4. 在这个答案中,我将概述(并提供)一个概念验证解析器。

      2。解决问题

      鉴于一个程序,我们如何破译其函数依赖性?

      catch

      为了理解一个程序,我们需要将它的代码分开,我们需要理解它的语义:我们需要一个解析器。我之所以选择acorn,是因为我从未使用它并听到过好评。我建议你稍微玩一下,看看SpiderMonkeys's AST中的程序是什么样的。

      现在我们有一个神奇的解析器将JavaScript转换为AST(Abstract Syntax Tree),我们将如何逻辑处理查找依赖关系?我们需要做两件事:

      1. 正确构建范围
      2. 了解函数调用引用的函数。
      3. 我们可以看到为什么上面的示例D可能含糊不清:有两个函数叫//A. just a global function globalFunction(); //B. a function within a function var outer = function () { function foo () {} foo(); }; //C. calling a function within itself var outer = function inner () { inner(); }; //D. disambiguating between two identically named functions function foo () { var foo = function () {}; foo(); } foo(); ,我们怎么知道哪个foo意味着什么?这就是我们需要实施范围界定的原因。

        3。解决问题

        由于解决方案分为两部分,让我们这样解决。从最大的问题开始:

        3.1。划定范围

        所以...我们有一个AST。它有一堆节点。我们如何建立范围?好吧,我们只关心功能范围。这简化了流程,因为我们知道我们只需要处理功能。但在我们讨论如何使用范围之前,让我们定义制作范围的函数。

        范围有什么作用?它不是一个复杂的存在:它有一个父范围(如果它是全局范围,则为foo()),并且它包含它包含的项目。我们需要一种方法来向范围中添加内容,并从一个方面获取内容。让我们这样做:

        null

        您可能已经注意到,我在两个方面作弊:首先,我指定了儿童范围。这是为了让我们更容易让人类看到事情正在发挥作用(否则,所有范围都是内部的,我们只能看到全局范围)。其次,我假设全局范围包含全部 - 也就是说,如果var Scope = function (parent) { var ret = { items : {}, parent : parent, children : [] }; ret.get = function (name) { if (this.items[name]) { return this.items[name]; } if (this.parent) { return this.parent.get(name); } //this is fake, as it also assumes every global reference is legit return name; }; ret.add = function (name, val) { this.items[name] = val; }; if (parent) { parent.children.push(ret); } return ret; }; 未在任何范围内定义,那么它必须是现有的全局变量。这可能是也可能不合适。

        好的,我们有办法表示范围。不要破解香槟,我们仍然必须真正制作它们!让我们看看AST中的简单函数声明foo如何:

        function f(){}

        那是相当满口的,但我们可以勇敢地度过它!多汁的部分是:

        {
          "type": "Program",
          "start": 0,
          "end": 14,
          "body": [{
            "type": "FunctionDeclaration",
            "start": 0,
            "end": 14,
            "id": {
              "type": "Identifier",
              "start": 9,
              "end": 10,
              "name": "f"
            },
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 12,
              "end": 14,
              "body": []
            }
          }]
        }
        

        我们有一个{ "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "f" }, "params": [ ... ], "body": { ... } } 节点,其FunctionDeclaration属性。 id的名称是我们的功能名称!假设我们有一个函数id负责遍历节点,walkcurrentScope变量,我们刚刚解析了函数声明currentFuncName。我们该怎么做呢?代码胜于雄辩:

        node

        但等等,函数表达式怎么样?他们的行为有点不同!首先,他们不一定有一个名字,如果他们这样做,它只会在他们内部可见:

        //save our state, so we will return to it after we handled the function
        var cachedScope = currentScope,
            cachedName = currentFuncName;
        
        //and now we change the state
        currentScope = Scope(cachedScope);
        currentFuncName = node.id.name;
        
        //create the bindings in the parent and current scopes
        //the following lines have a serious bug, we'll get to it later (remember that
        // we have to meet Captain Crunchypants)
        cachedScope.add(currentFuncName, currentName);
        currentScope.add(currentFuncName, currentName);
        
        //continue with the parsing
        walk(node.body);
        
        //and restore the state
        currentScope = cachedScope;
        currentFuncName = cachedName;
        

        让我们做出另一个巨大的假设,即我们已经处理了变量声明部分 - 我们在父作用域创建了正确的绑定。然后,上面用于处理函数的逻辑只是略有改变:

        var outer = function inner () {
            //outer doesn't exist, inner is visible
        };
        //outer is visible, inner doesn't exist
        

        不管你信不信,这或多或少都是最终解决方案中的整个范围处理机制。我希望当你添加像对象这样的东西时,它会变得更加复杂,但它并不是很多。

        是时候见到Crunchpants船长了。非常敏锐的倾听者现在已经记住了例子D.让我们回忆起我们的记忆:

        ...
        //and now we change the state
        currentScope = Scope(cachedScope);
        //we  signify anonymous functions with <anon>, since a function can never be called that
        currentFuncName = node.id ? node.id.name : '<anon>';
        ...
        if (node.id) {
            currentScope.add(currentFuncName, currentFuncName);
        }
        if (node.type === 'FunctionDeclaration') {
            cachedScope.add(currentFuncName, currentFuncName);
        }
        ...
        

        在解析时,我们需要一种方法来区分外部function foo () { function foo () {} foo(); } foo(); 和内部foo - 否则,我们无法知道这些foo中的哪一个电话,我们的依赖查找器将是吐司。此外,我们无法在依赖关系管理中区分它们 - 如果我们只是按功能名称添加结果,我们将被覆盖。换句话说,我们需要一个绝对函数名称。

        我选择用foo字符表示分隔嵌套。然后,上面有一个函数#,内部函数foo,调用foo#foo和调用foo#foo。或者,对于一个不那么令人困惑的例子:

        foo

        有一个函数var outer = function () { function inner () {} inner(); }; outer(); 和一个函数outer。拨打outer#inner并致电outer#inner

        所以,让我们创建这个函数,该函数采用以前的名称和当前函数的名称,并将它们组合在一起:

        outer

        修改我们的函数处理伪代码(即将生效!我保证!):

        function nameToAbsolute (parent, child) {
            //foo + bar => foo#bar
            if (parent) {
                return parent + '#' + name;
            }
            return name;
        }
        

        现在我们正在谈论!是时候继续实际事了!也许我一直对你撒谎,我什么都不知道,也许我悲惨地失败了,直到现在我还在继续写作,因为我知道没有人会读到这么远,而且我会得到很多赞成,因为它很长解答!?

        HAH!坚持下去!还有更多未来!我无缘无故地坐了几天! (作为一个有趣的社交实验,任何人都可以对评论进行评论,并在线条上说些什么&#34; Crunchpants队长很高兴见到你&#34;?)

        更严重的是,我们应该开始制作解析器:什么保持我们的状态并遍历节点。由于我们在最后,范围和依赖关系中都有两个解析器,我们将制作一个&#34;主解析器&#34;在需要时调用每个人:

        ...
        currentScope = Scope(cachedScope);
        var name = node.id ? node.id.name : '<anon>';
        currentFuncName = nameToAbsolute(cachedName, name);
        ...
        if (node.id) {
            currentScope.add(name, currentFuncName);
        }
        if (node.type === 'FunctionDeclaration') {
            cachedScope.add(name, currentFuncName);
        }
        

        这有点儿残忍,但希望这是可以理解的。我们将var parser = { results : {}, state : {}, parse : function (string) { this.freshen(); var root = acorn.parse(string); this.walk(root); return this.results; }, freshen : function () { this.results = {}; this.results.deps = {}; this.state = {}; this.state.scope = this.results.scope = Scope(null); this.state.name = ''; }, walk : function (node) { //insert logic here }, // '' => 'foo' // 'bar' => 'bar#foo' nameToAbsolute : function (parent, name) { return parent ? parent + '#' + name : name; }, cacheState : function () { var subject = this.state; return Object.keys( subject ).reduce(reduce, {}); function reduce (ret, key) { ret[key] = subject[key]; return ret; } }, restoreState : function (st) { var subject = this.state; Object.keys(st).forEach(function (key) { subject[key] = st[key]; }); } }; 设置为一个对象,为了使其灵活,statecacheState只是克隆/合并。

        现在,我们心爱的restoreState

        scopeParser

        随便观察的读者会注意到var scopeParser = { parseFunction : function (func) { var startState = parser.cacheState(), state = parser.state, name = node.id ? node.id.name : '<anon>'; state.scope = Scope(startState.scope); state.name = parser.nameToAbsolute(startState.name, name); if (func.id) { state.scope.add(name, state.name); } if (func.type === 'FunctionDeclaration') { startState.scope.add(name, state.name); } this.addParamsToScope(func); parser.walk(func.body); parser.restoreState(startState); } }; 是空的。是时候填补了!

        parser.walk

        再次,主要是技术性 - 要理解这些,你需要玩橡子。我们希望确保迭代并正确地进入节点。表达式walk : function (node) { var type = node.type; //yes, this is tight coupling. I will not apologise. if (type === 'FunctionDeclaration' || type === 'FunctionExpression') { scopeParser.parseFunction(node) } else if (node.type === 'ExpressionStatement') { this.walk(node.expression); } //Program, BlockStatement, ... else if (node.body && node.body.length) { node.body.forEach(this.walk, this); } else { console.log(node, 'pass through'); } //...I'm sorry } 之类的节点具有我们走过的(function foo() {})属性,expression节点(例如函数的实际主体)和程序节点具有BlockStatement数组等。< / p>

        由于我们有类似逻辑的东西,让我们试试:

        body

        纯!使用函数声明和表达式,看看它们是否正确嵌套。但是我们忘了包含变量声明:

        > parser.parse('function foo() {}').scope
        { items: { foo: 'foo' },
          parent: null,
          children:
           [ { items: [Object],
               parent: [Circular],
               children: [],
               get: [Function],
               add: [Function] } ],
          get: [Function],
          add: [Function] }
        

        一个好的(和有趣的!)练习是自己添加它们。但不要担心 - 他们将被包含在最终的解析器中;

        谁相信!?我们用范围完成 ! d-O-N-E!让我们欢呼吧!

        哦,哦,哦......你认为你在哪里去了!?我们只解决了部分问题 - 我们仍然需要找到依赖项!或者你忘了它的一切!?好的,你可以去厕所。但最好是#1。

        3.2。依赖

        哇,你甚至还记得我们有节号吗?在一个不相关的说明中,当我输入最后一句时,我的键盘发出的声音让人想起超级马里奥主题曲的第一个音符。现在,我的脑海里浮现了。

        确定!所以,我们有我们的范围,我们有我们的功能名称,是时候识别函数调用了!这不会花很长时间。做var foo = function () {}; bar = function () {}; 会给出:

        acorn.parse('foo()')

        所以我们正在寻找{ "type": "Program", "body": [{ "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "f" }, "arguments": [] } }] } 。但在我们全部CallExpression之前,让我们首先回顾一下我们的逻辑。鉴于此节点,我们该怎么办?我们如何添加依赖项?

        这不是一个难题,因为我们已经处理了所有的范围。我们将包含函数(walk)的依赖关系添加到parser.state.name的范围解析中。听起来很简单!

        callExpression.callee.name

        再一次,处理全球背景的伎俩。如果当前状态是无名的,我们假设它是全局上下文并为其指定一个特殊名称var deps = parser.results.deps, scope = parser.state.scope, context = parser.state.name || '<global>'; if (!deps[context]) { deps[context] = []; } deps[context].push(scope.get(node.callee.name));

        现在我们有了这个,让我们构建我们的<global>

        dependencyParser

        真的很漂亮。我们仍需要修改var dependencyParser = { parseCall : function (node) { ...the code above... } }; 以包含parser.walk s:

        CallExpression

        在示例D中尝试一下:

        walk : function (node) {
            ...
            else if (type === 'CallExpression') {
                dependencyParser.parseCall(node);
            }
        }
        

        4。嘲笑问题

        HAHA!在你的脸上,问题! WOOOOOOOOOOO!

        您可以开始庆祝活动。脱掉你的裤子,在城里跑来跑去,声称你是镇上的鸡肉并烧掉流浪垃圾桶( Zirak和Affiliates绝不支持任何种类或不雅的暴露。任何采取的行动哦,说任何读者都不应该受到Zirak和/或关联公司的指责)。

        但现在认真。我们解决了一个非常非常有限的问题子集,并且为了解决一小部分实际情况,需要做很多事情。这不是沮丧 - 恰恰相反!我劝你试着这样做。很有趣! ( Zirak及其关联公司对因尝试刚才所说的而导致的精神崩溃概不负责)

        这里提供的是解析器的源代码,没有任何NodeJS特定的东西(即需要acorn或暴露解析器):

        > parser.parse('function foo() { var foo = function () {}; foo(); } foo()').deps
        { foo: [ 'foo#foo' ], '<global>': [ 'foo' ] }
        

        现在,如果你原谅我,我需要长时间洗澡。

答案 1 :(得分:8)

很抱歉,在使用eval的动态语言中,这在理论上是不可能的。好的IDE可以检测基本的东西,但有些东西你根本检测得不好:

让我们看看你的简单案例:

function a() {
   //do something
   b();
};

让我们稍微复杂一点:

function a() {
   //do something
   eval("b();")
};

现在我们必须检测字符串中的内容,让我们先走一步:

function a() {
   //do something
   eval("b"+"();");
};

现在我们必须检测字符串concats的结果。让我们再做几个:

function a() {
   //do something
   var d = ["b"];
   eval(d.join("")+"();");
};

还不开心吗?我们编码吧:

function a() {
   //do something
   var d = "YigpOw==";
   eval(atob(d));
};

现在,这些是一些非常基本的案例,我可以根据需要使它们复杂化。 确实无法运行代码 - 您必须在每个可能的输入上运行它并检查,我们都知道这是不切实际的。

那你能做什么?

将依赖关系作为参数传递给函数并使用控制反转。始终明确您的更复杂的依赖关系而不是隐含的。这样你就不需要工具来知道你的依赖是什么了:)

答案 2 :(得分:3)

您可以使用统计分析器日志(node --prof yourprogram,v8.log)来计算“统计”调用图。查看日志处理器源代码herehere

答案 3 :(得分:1)

  1. 以字符串形式获取函数代码:a.toString()
  2. 使用RegEx检查可能的函数调用,例如possiblefuncname(possiblefuncname.call(以及possiblefuncname.apply(
  3. 检查`typeof possiblefuncname =='function'
  4. IF 3为TRUE,递归检查依赖项的possiblefuncname
  5. 设置您的依赖关系。