我试图全面了解我的问题。我需要编写一个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的工具或插件?
答案 0 :(得分:47)
(我提前道歉:我经常试着让我的答案变得幽默,以便通过它们让读者感到轻松,但在这种情况下,我无法成功地做到这一点。请考虑对此答案的长度采取双重道歉。 )
这不是一个容易的问题。我们不会完全解决它,而是限制其范围 - 我们只会解决我们关心的问题部分。我们将通过使用JavaScript解析器解析输入并使用简单的recurive-descent算法对其进行检查来实现。我们的算法将分析程序的范围并正确识别函数调用。
其余的只是填补空白!结果位于答案的底部,因此如果您不想通读,我建议您抓住first comment。
正如Benjamin Gruenbaum's answer所说,由于JavaScript的动态特性,这是一个非常非常困难的问题。但是,如果不是制定一个适用于100%程序的解决方案,我们会为一部分程序做这件事,如果我们限制自己来处理某些事情呢?
最重要的限制:
eval
。如果我们加入eval
,那就会陷入混乱。这是因为eval允许您使用任意字符串,这使得跟踪依赖性成为不可能,而无需检查每个可能的输入。在NodeJS中没有document.write
和setTimeout
只接受一个功能,所以我们不必担心这些。但是,我们也不允许使用vm module。以下限制是为了简化流程。它们可能是可以解决的,但是解决这些问题超出了这个答案的范围:
obj[key]()
让我很难介绍这个限制,但在某些情况下它肯定是可以解决的(例如key = 'foo'
但不是{{1} })key = userInput()
。绝对可以使用完整的范围解析器解决。var self = this
最后,在这个答案中对实现的限制 - 要么是因为复杂性约束,要么是时间限制(但它们是非常容易解决的):
(a, b)()
或foo.bar()
之类的事情至少会使程序复杂性翻倍。投入足够的时间,这是非常可行的。this.foo()
语句,with
块)。我们没有处理它们。在这个答案中,我将概述(并提供)一个概念验证解析器。
鉴于一个程序,我们如何破译其函数依赖性?
catch
为了理解一个程序,我们需要将它的代码分开,我们需要理解它的语义:我们需要一个解析器。我之所以选择acorn,是因为我从未使用它并听到过好评。我建议你稍微玩一下,看看SpiderMonkeys's AST中的程序是什么样的。
现在我们有一个神奇的解析器将JavaScript转换为AST(Abstract Syntax Tree),我们将如何逻辑处理查找依赖关系?我们需要做两件事:
我们可以看到为什么上面的示例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
意味着什么?这就是我们需要实施范围界定的原因。
由于解决方案分为两部分,让我们这样解决。从最大的问题开始:
所以...我们有一个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
负责遍历节点,walk
和currentScope
变量,我们刚刚解析了函数声明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];
});
}
};
设置为一个对象,为了使其灵活,state
和cacheState
只是克隆/合并。
现在,我们心爱的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。
确定!所以,我们有我们的范围,我们有我们的功能名称,是时候识别函数调用了!这不会花很长时间。做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);
}
}
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)
答案 3 :(得分:1)
a.toString()
possiblefuncname(
和possiblefuncname.call(
以及possiblefuncname.apply(