[英]How can I detect all dependencies of a function in Node.js?
我试图全面了解我的问题。 我需要编写一个Node.js程序,它应该能够检测函数的所有依赖项。
例如
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的工具或插件?
(我提前道歉:我通常会尝试让我的回答变得幽默,以便通过它们让读者感到轻松,但在这种情况下我无法成功地做到这一点。请考虑对此答案的长度采取双重道歉。)
这不是一个容易的问题。 我们不会完全解决它,而是限制其范围 - 我们只会解决我们关心的问题部分。 我们将通过使用JavaScript解析器解析输入并使用简单的recurive-descent算法进行检查来实现。 我们的算法将分析程序的范围并正确识别函数调用。
所有其余的只是填补空白! 结果在答案的底部,所以如果您不想阅读,我建议您抓住第一条评论 。
正如Benjamin Gruenbaum的回答所说,由于JavaScript的动态特性,这是一个非常非常困难的问题。 但是,如果我们限制自己处理某些事情,如果不是为100%的程序提供解决方案,而是为一部分程序而做,那该怎么办呢?
最重要的限制:
eval
。 如果我们包括eval
,它就会陷入混乱。 这是因为eval允许你使用任意字符串,这使得跟踪依赖性成为不可能,而无需检查每个可能的输入。 在NodeJS中没有document.write
和setTimeout
只接受一个函数,所以我们不必担心这些。 但是,我们也不允许使用vm模块 。 以下限制是为了简化该过程。 它们可能是可以解决的,但解决它们超出了这个答案的范围:
obj[key]()
让我很难介绍这个限制,但它确实可以解决某些情况(例如key = 'foo'
但不是key = userInput()
) var self = this
。 绝对可以使用完整的范围解析器解决。 (a, b)()
最后,在这个答案中实现的限制 - 要么是因为复杂性约束或时间限制(但它们是非常可解的):
foo.bar()
或this.foo()
类的东西至少会使程序复杂性增加一倍。 投入足够的时间,这是非常可行的。 with
语句, catch
块)。 我们不处理它们。 在这个答案中,我将概述(并提供)一个概念验证解析器。
给定一个程序,我们如何破译其函数依赖?
//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();
为了理解一个程序,我们需要将它的代码分开,我们需要理解它的语义:我们需要一个解析器。 我选择了橡子,因为我从未使用它并听到了好评。 我建议你稍微玩一下,看看SpiderMonkeys的AST中的程序是什么样的 。
现在我们有一个神奇的解析器将JavaScript转换为AST(一个抽象语法树 ),我们将如何逻辑处理查找依赖关系? 我们需要做两件事:
我们可以看到为什么上面的例子D可能是模棱两可的:有两个函数叫做foo
,我们怎么知道哪一个foo()
意味着什么? 这就是我们需要实施范围界定的原因。
由于解决方案分为两部分,让我们这样解决。 从最大的问题开始:
所以...我们有一个AST。 它有一堆节点。 我们如何建立范围? 好吧,我们只关心功能范围。 这简化了流程,因为我们知道我们只需要处理功能。 但在我们讨论如何使用范围之前,让我们定义制作范围的函数。
范围有什么作用? 这不是一个复杂的存在:它有一个父范围(如果它是全局范围,则为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;
};
您可能已经注意到,我在两个方面作弊:首先,我正在分配子范围。 这是为了让我们更容易让人类看到事情正在发挥作用(否则,所有范围都是内部的,我们只看到全局范围)。 其次,我假设全局范围包含all - 也就是说,如果foo
未在任何范围内定义,那么它必须是现有的全局变量。 这可能是也可能不是可取的。
好的,我们有办法表示范围。 不要破开香槟,我们仍然需要实际制作它们! 让我们看看一个简单的函数声明, function f(){}
在AST中是什么样的:
{
"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": { ... }
}
我们有一个带有id
属性的FunctionDeclaration
节点。 那个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
让我们做另一个巨大的假设,我们已经处理了变量声明部分 - 我们在父作用域创建了正确的绑定。 然后,上面用于处理函数的逻辑只是略有改变:
...
//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);
}
...
不管你信不信,这或多或少都是最终解决方案中的整个范围处理机制。 我希望当你添加像对象这样的东西时,它会变得更加复杂,但它并不是很多。
是时候见到Crunchpants船长了。 非常敏锐的倾听者现在已经记住了例子D.让我们回忆起我们的记忆:
function foo () {
function foo () {}
foo();
}
foo();
在解析它时,我们需要一种方法来区分外部foo
和内部foo
- 否则,我们将无法知道这些foo
调用中的哪一个,并且我们的依赖查找器将是toast。 此外,我们将无法在依赖关系管理中区分它们 - 如果我们只是按函数名称添加结果,我们将被覆盖。 换句话说,我们需要一个绝对的函数名称。
我选择用#
字符表示分隔嵌套。 然后,上面有一个函数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;
}
并修改我们的函数处理伪代码(即将生效!我保证!):
...
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);
}
现在我们正在谈论! 现在是时候继续实际做某事了! 也许我一直对你撒谎,我一无所知,也许我悲惨地失败了,我继续写作直到现在,因为我知道没有人会读到这么远,我会得到许多赞成,因为这是一个很长的答案!?
哈! 坚持下去! 还有更多未来! 我无缘无故地坐了几天! (作为一个有趣的社交实验,任何人都可以对评论进行评论,说出“Crunchpants队长很高兴见到你”这句话吗?)
更严重的是,我们应该开始创建解析器 :什么保持我们的状态并遍历节点。 由于我们最后会有两个解析器,范围和依赖,我们将创建一个“主解析器”,在需要时调用每个解析器:
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
节点(例如函数的实际主体)和程序节点具有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] }
整齐! 使用函数声明和表达式,看看它们是否正确嵌套。 但是我们确实忘了包含变量声明:
var foo = function () {};
bar = function () {};
一个好的(和有趣的!)练习是自己添加它们。 但不要担心 - 它们将包含在最终解析器中;
谁相信!? 我们完成了范围! DONE! 我们欢呼吧!
哦,哦,哦......你认为你要去哪儿了?? 我们只解决了部分问题 - 我们仍然需要找到依赖项! 或者你忘了它的一切!? 好的,你可以去厕所。 但它最好是#1。
哇,你还记得我们有段号吗? 在一个不相关的说明中,当我输入最后一句时,我的键盘发出的声音让人想起超级马里奥主题曲的第一个音符。 现在哪个被困在我脑海里了。
好! 所以,我们有我们的范围,我们有我们的函数名称,是时候识别函数调用了! 这不会花很长时间。 做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
以包含CallExpression
:
walk : function (node) {
...
else if (type === 'CallExpression') {
dependencyParser.parseCall(node);
}
}
并在示例D中尝试一下:
> parser.parse('function foo() { var foo = function () {}; foo(); } foo()').deps
{ foo: [ 'foo#foo' ], '<global>': [ 'foo' ] }
哈哈! 在你的脸上,问题! WOOOOOOOOOOO!
你可以开始庆祝活动。 脱掉你的裤子,在城里跑来跑去,声称你是镇上的鸡肉并烧掉流浪垃圾桶( Zirak和Affiliates绝不支持任何种类或不雅暴露。任何采取的行动哦,比方说,任何读者都不是归咎于Zirak和/或关联公司 )。
但现在认真。 我们解决了一个非常非常有限的问题子集,并且为了解决一小部分实际情况,需要做很多事情。 这不是沮丧 - 恰恰相反! 我劝你试着这样做。 好有趣! ( Zirak及其关联公司对由于试图刚才所说的事情导致的精神崩溃无法承担任何责任 )
这里提供的是解析器的源代码,没有任何NodeJS特定的东西(即需要acorn或暴露解析器):
var parser = {
results : {},
state : {},
verbose : false,
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) {
var type = node.type;
//yes, this is tight coupling. I will not apologise.
if (type === 'FunctionDeclaration' || type === 'FunctionExpression') {
scopeParser.parseFunction(node)
}
else if (type === 'AssignmentExpression') {
scopeParser.parseBareAssignmentExpression(node);
}
else if (type === 'VariableDeclaration') {
scopeParser.parseVarDeclaration(node);
}
else if (type === 'CallExpression') {
dependencyParser.parseCall(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 if (this.verbose) {
console.log(node, 'pass through');
}
//...I'm sorry
},
// '' => '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];
});
}
};
var dependencyParser = {
//foo()
//yes. that's all.
parseCall : function (node) {
if (parser.verbose) {
console.log(node, 'parseCall');
}
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));
}
};
var scopeParser = {
// We only care about these kinds of tokens:
// (1) Function declarations
// function foo () {}
// (2) Function expressions assigned to variables
// var foo = function () {};
// bar = function () {};
//
// Do note the following property:
// var foo = function bar () {
// `bar` is visible, `foo` is not
// };
// `bar` is not visible, `foo` is
/*
function foo () {}
=>
{
"type": 'FunctionDeclaration',
"id": {
"type": Identifier,
"name": 'foo'
},
"params": [],
"body": { ... }
}
(function () {})
=>
{
"type": "FunctionExpression",
"id": null,
"params": [],
"body": { ... }
}
*/
parseFunction : function (func) {
if (parser.verbose) {
console.log(func, 'parseFunction');
}
var startState = parser.cacheState(),
state = parser.state,
name = this.grabFuncName(func);
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);
},
grabFuncName : function (func) {
if (func.id) {
return func.id.name;
}
else if (func.type === 'FunctionExpression') {
return '<anon>';
}
else {
//...this shouldn't happen
throw new Error(
'scope.parseFunction encountered an anomalous function: ' +
'nameless and is not an expression');
}
},
/*
[{
"type": "Identifier",
"name": "a"
}, {
"type": "Identifier",
"name": "b"
}, {
"type": "Identifier",
"name": "c"
}]
*/
addParamsToScope : function (func) {
var scope = parser.state.scope,
fullName = parser.state.name;
func.params.forEach(addParam);
function addParam (param) {
var name = param.name;
scope.add(name, parser.nameToAbsolute(fullName, name));
}
},
parseVarDeclaration : function (tok) {
if (parser.verbose) {
console.log(tok, 'parseVarDeclaration');
}
tok.declarations.forEach(parseDecl, this);
function parseDecl (decl) {
this.parseAssignment(decl.id, decl.init);
}
},
// Lacking a better name, this:
// foo = function () {}
// without a `var`, I call a "bare assignment"
parseBareAssignmentExpression : function (exp) {
if (parser.verbose) {
console.log(exp, 'parseBareAssignmentExpression');
}
this.parseAssignment(exp.left, exp.right);
},
parseAssignment : function (id, value) {
if (parser.verbose) {
console.log(id, value, 'parseAssignment');
}
if (!value || value.type !== 'FunctionExpression') {
return;
}
var name = id.name,
val = parser.nameToAbsolute(parser.state.name, name);
parser.state.scope.add(name, val);
this.parseFunction(value);
}
};
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;
};
现在,如果你能原谅我,我需要长时间淋浴。
没有。
对不起,这在使用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));
};
现在,这些是一些非常基本的案例,我可以根据需要使它们复杂化。 实际上没有办法运行代码 - 你必须在每个可能的输入上运行它并检查,我们都知道这是不切实际的。
将依赖关系作为参数传递给函数并使用控制反转。 始终明确您的更复杂的依赖关系而不是隐含的。 这样你就不需要工具来知道你的依赖是什么:)
a.toString()
possiblefuncname(
和possiblefuncname.call(
和possiblefuncname.apply(
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.