@dacely/toildefender 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -661
- package/NOTICE.md +16 -16
- package/README.md +467 -380
- package/cli.js +168 -168
- package/defendjs.js +6 -6
- package/docs/all-modes-output.demo.js +673 -673
- package/estest.js +49 -49
- package/esutils.js +107 -107
- package/logger.js +28 -28
- package/obfuscator.js +534 -534
- package/package.json +108 -108
- package/processors/deadCode.js +62 -62
- package/processors/flattener.js +808 -808
- package/processors/health.js +55 -55
- package/processors/identifiers.js +256 -256
- package/processors/literalObfuscator.js +40 -40
- package/processors/literals.js +233 -233
- package/processors/methods.js +332 -332
- package/processors/modules.js +231 -231
- package/processors/normalizer.js +490 -490
- package/processors/numericVm.js +950 -950
- package/processors/postprocessing.js +69 -69
- package/processors/preprocessing.js +248 -248
- package/processors/scopes.js +177 -177
- package/processors/uglifier.js +25 -25
- package/processors/variables.js +185 -185
- package/toildefender.js +7 -7
- package/traverser.js +115 -115
- package/utils.js +135 -135
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var assert = require("assert");
|
|
4
|
-
|
|
5
|
-
var esprima = require("esprima");
|
|
6
|
-
|
|
7
|
-
module.exports = class LiteralObfuscator {
|
|
8
|
-
|
|
9
|
-
constructor (logger) {
|
|
10
|
-
this.logger = logger;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generate an obfuscated string generator
|
|
15
|
-
* @param {string} input
|
|
16
|
-
* @returns {Node}
|
|
17
|
-
*/
|
|
18
|
-
obfuscateString1 (input) {
|
|
19
|
-
assert.equal(typeof input, "string");
|
|
20
|
-
|
|
21
|
-
function is16Bit (s) {
|
|
22
|
-
return s.split("").some(x => x.charCodeAt(0) > 65536);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
var getCharCode = function (x) { return x.charCodeAt(0); };
|
|
26
|
-
var chars = input.split("").map(getCharCode);
|
|
27
|
-
|
|
28
|
-
var out = [];
|
|
29
|
-
for (var i = 0; i < input.length; i += 2) {
|
|
30
|
-
var n = chars[i] | (chars[i + 1] << 16);
|
|
31
|
-
out.push(n);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return esprima.parse(`
|
|
35
|
-
var input = ${JSON.stringify(out)};
|
|
36
|
-
input.map(function(x) { return String.fromCharCode(x & ~0 >>> 16) + String.fromCharCode(x >> 16); }).join("");
|
|
37
|
-
`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var assert = require("assert");
|
|
4
|
+
|
|
5
|
+
var esprima = require("esprima");
|
|
6
|
+
|
|
7
|
+
module.exports = class LiteralObfuscator {
|
|
8
|
+
|
|
9
|
+
constructor (logger) {
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate an obfuscated string generator
|
|
15
|
+
* @param {string} input
|
|
16
|
+
* @returns {Node}
|
|
17
|
+
*/
|
|
18
|
+
obfuscateString1 (input) {
|
|
19
|
+
assert.equal(typeof input, "string");
|
|
20
|
+
|
|
21
|
+
function is16Bit (s) {
|
|
22
|
+
return s.split("").some(x => x.charCodeAt(0) > 65536);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var getCharCode = function (x) { return x.charCodeAt(0); };
|
|
26
|
+
var chars = input.split("").map(getCharCode);
|
|
27
|
+
|
|
28
|
+
var out = [];
|
|
29
|
+
for (var i = 0; i < input.length; i += 2) {
|
|
30
|
+
var n = chars[i] | (chars[i + 1] << 16);
|
|
31
|
+
out.push(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return esprima.parse(`
|
|
35
|
+
var input = ${JSON.stringify(out)};
|
|
36
|
+
input.map(function(x) { return String.fromCharCode(x & ~0 >>> 16) + String.fromCharCode(x >> 16); }).join("");
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
};
|
package/processors/literals.js
CHANGED
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var assert = require("assert");
|
|
4
|
-
|
|
5
|
-
var _ = require("lodash");
|
|
6
|
-
|
|
7
|
-
var estest = require("../estest");
|
|
8
|
-
var traverser = require("../traverser");
|
|
9
|
-
var utils = require("../utils");
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate string generator from string.
|
|
13
|
-
* @param {string} str
|
|
14
|
-
* @returns {Node}
|
|
15
|
-
*/
|
|
16
|
-
function makeStringGenerator(str) {
|
|
17
|
-
assert.equal(typeof str, "string");
|
|
18
|
-
|
|
19
|
-
var fragments = [];
|
|
20
|
-
|
|
21
|
-
while (str.length > 0) {
|
|
22
|
-
var len = utils.random(1, 5);
|
|
23
|
-
fragments.push(str.substring(0, len));
|
|
24
|
-
str = str.substring(len);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
var block = {
|
|
28
|
-
type: "BlockStatement",
|
|
29
|
-
body: [
|
|
30
|
-
{
|
|
31
|
-
type: "VariableDeclaration",
|
|
32
|
-
kind: "var",
|
|
33
|
-
declarations: [
|
|
34
|
-
{
|
|
35
|
-
type: "VariableDeclarator",
|
|
36
|
-
id: { type: "Identifier", name: "str" },
|
|
37
|
-
init: { type: "Literal", value: "" }
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
]
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
fragments.forEach(fragment => {
|
|
45
|
-
var decoded = makeStringByteArrayCall(fragment);
|
|
46
|
-
|
|
47
|
-
block.body.push({
|
|
48
|
-
type: "ExpressionStatement",
|
|
49
|
-
expression: {
|
|
50
|
-
type: "BinaryExpression",
|
|
51
|
-
operator: "+=",
|
|
52
|
-
left: { type: "Identifier", name: "str" },
|
|
53
|
-
right: decoded
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
block.body.push({
|
|
59
|
-
type: "ReturnStatement",
|
|
60
|
-
argument: { type: "Identifier", name: "str" }
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
type: "CallExpression",
|
|
65
|
-
arguments: [],
|
|
66
|
-
callee: {
|
|
67
|
-
type: "FunctionExpression",
|
|
68
|
-
params: [],
|
|
69
|
-
body: block
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Generate unicode-escaped string generator from string.
|
|
76
|
-
* @param {string} str
|
|
77
|
-
* @returns {Node}
|
|
78
|
-
*/
|
|
79
|
-
function makeStringUnicode(str) {
|
|
80
|
-
assert.equal(typeof str, "string");
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
type: "CallExpression",
|
|
84
|
-
callee: { type: "Identifier", name: "eval" },
|
|
85
|
-
arguments: [
|
|
86
|
-
{
|
|
87
|
-
type: "Literal",
|
|
88
|
-
value: "\"" + str.split("").map(x => "\\x" + x.charCodeAt().toString(16)).join("") + "\""
|
|
89
|
-
}
|
|
90
|
-
]
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Generate URL-escaped string generator from string.
|
|
96
|
-
* @param {string} str
|
|
97
|
-
* @returns {Node}
|
|
98
|
-
*/
|
|
99
|
-
function makeStringUnescape(str) {
|
|
100
|
-
assert.equal(typeof str, "string");
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
type: "CallExpression",
|
|
104
|
-
callee: { type: "Identifier", name: "unescape" },
|
|
105
|
-
arguments: [
|
|
106
|
-
{
|
|
107
|
-
type: "Literal",
|
|
108
|
-
value: str.split("").map(x => "%" + x.charCodeAt().toString(16)).join("")
|
|
109
|
-
}
|
|
110
|
-
]
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Generate char-code-escaped char generator from char.
|
|
116
|
-
* @param {string} cha
|
|
117
|
-
* @returns {Node}
|
|
118
|
-
*/
|
|
119
|
-
function makeCharByte(cha) {
|
|
120
|
-
assert.equal(typeof cha, "string");
|
|
121
|
-
assert.equal(cha.length, 1);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
type: "CallExpression",
|
|
125
|
-
callee: {
|
|
126
|
-
type: "MemberExpression",
|
|
127
|
-
computed: false,
|
|
128
|
-
object: { type: "Identifier", name: "String" },
|
|
129
|
-
property: { type: "Identifier", name: "fromCharCode" }
|
|
130
|
-
},
|
|
131
|
-
arguments: [
|
|
132
|
-
{
|
|
133
|
-
type: "Literal",
|
|
134
|
-
value: cha.charCodeAt(0)
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generate char-code-escaped string generator from string.
|
|
142
|
-
* @param {string} str
|
|
143
|
-
* @returns {Node}
|
|
144
|
-
*/
|
|
145
|
-
function makeStringByteArrayCall(str) {
|
|
146
|
-
assert.equal(typeof str, "string");
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
type: "CallExpression",
|
|
150
|
-
callee: { type: "Identifier", name: "veilmark$fromCharCodes" },
|
|
151
|
-
arguments: str.split("").map(x => ({ type: "Literal", value: x.charCodeAt() }))
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
module.exports = class Literals {
|
|
156
|
-
|
|
157
|
-
constructor (logger) {
|
|
158
|
-
this.logger = logger;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Move strings into $$strings array
|
|
163
|
-
* @param {Node} ast Root node
|
|
164
|
-
* @returns {Node} Root node
|
|
165
|
-
*/
|
|
166
|
-
extractStrings (ast) {
|
|
167
|
-
assert.ok(estest.isNode(ast));
|
|
168
|
-
|
|
169
|
-
var global = { type: "Identifier", name: "$$strings" };
|
|
170
|
-
|
|
171
|
-
var strings = [];
|
|
172
|
-
var stringMap = {};
|
|
173
|
-
|
|
174
|
-
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
175
|
-
if (node.type == "Literal" && typeof node.value == "string") {
|
|
176
|
-
var idx = stringMap["_" + node.value];
|
|
177
|
-
if (!idx) {
|
|
178
|
-
stringMap["_" + node.value] = idx = strings.length;
|
|
179
|
-
strings.push(node);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
type: "MemberExpression",
|
|
184
|
-
computed: true,
|
|
185
|
-
object: global,
|
|
186
|
-
property: { type: "Literal", value: idx }
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return node;
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
ast.body.splice(0, 0, {
|
|
194
|
-
type: "VariableDeclaration",
|
|
195
|
-
kind: "var",
|
|
196
|
-
declarations: [
|
|
197
|
-
{
|
|
198
|
-
type: "VariableDeclarator",
|
|
199
|
-
id: global,
|
|
200
|
-
init: {
|
|
201
|
-
type: "ArrayExpression",
|
|
202
|
-
elements: strings
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
return ast;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Replace string literals with string generators.
|
|
213
|
-
* @param {Node} ast Root node
|
|
214
|
-
* @returns {Node} Root node
|
|
215
|
-
*/
|
|
216
|
-
generateStrings (ast) {
|
|
217
|
-
assert.ok(estest.isNode(ast));
|
|
218
|
-
|
|
219
|
-
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
220
|
-
if (node.type == "Literal"
|
|
221
|
-
&& typeof node.value == "string"
|
|
222
|
-
&& stack.length > 1
|
|
223
|
-
&& stack[1].node.type != "Property") {
|
|
224
|
-
return makeStringGenerator(node.value);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return node;
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
return ast;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var assert = require("assert");
|
|
4
|
+
|
|
5
|
+
var _ = require("lodash");
|
|
6
|
+
|
|
7
|
+
var estest = require("../estest");
|
|
8
|
+
var traverser = require("../traverser");
|
|
9
|
+
var utils = require("../utils");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate string generator from string.
|
|
13
|
+
* @param {string} str
|
|
14
|
+
* @returns {Node}
|
|
15
|
+
*/
|
|
16
|
+
function makeStringGenerator(str) {
|
|
17
|
+
assert.equal(typeof str, "string");
|
|
18
|
+
|
|
19
|
+
var fragments = [];
|
|
20
|
+
|
|
21
|
+
while (str.length > 0) {
|
|
22
|
+
var len = utils.random(1, 5);
|
|
23
|
+
fragments.push(str.substring(0, len));
|
|
24
|
+
str = str.substring(len);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var block = {
|
|
28
|
+
type: "BlockStatement",
|
|
29
|
+
body: [
|
|
30
|
+
{
|
|
31
|
+
type: "VariableDeclaration",
|
|
32
|
+
kind: "var",
|
|
33
|
+
declarations: [
|
|
34
|
+
{
|
|
35
|
+
type: "VariableDeclarator",
|
|
36
|
+
id: { type: "Identifier", name: "str" },
|
|
37
|
+
init: { type: "Literal", value: "" }
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fragments.forEach(fragment => {
|
|
45
|
+
var decoded = makeStringByteArrayCall(fragment);
|
|
46
|
+
|
|
47
|
+
block.body.push({
|
|
48
|
+
type: "ExpressionStatement",
|
|
49
|
+
expression: {
|
|
50
|
+
type: "BinaryExpression",
|
|
51
|
+
operator: "+=",
|
|
52
|
+
left: { type: "Identifier", name: "str" },
|
|
53
|
+
right: decoded
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
block.body.push({
|
|
59
|
+
type: "ReturnStatement",
|
|
60
|
+
argument: { type: "Identifier", name: "str" }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
type: "CallExpression",
|
|
65
|
+
arguments: [],
|
|
66
|
+
callee: {
|
|
67
|
+
type: "FunctionExpression",
|
|
68
|
+
params: [],
|
|
69
|
+
body: block
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate unicode-escaped string generator from string.
|
|
76
|
+
* @param {string} str
|
|
77
|
+
* @returns {Node}
|
|
78
|
+
*/
|
|
79
|
+
function makeStringUnicode(str) {
|
|
80
|
+
assert.equal(typeof str, "string");
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
type: "CallExpression",
|
|
84
|
+
callee: { type: "Identifier", name: "eval" },
|
|
85
|
+
arguments: [
|
|
86
|
+
{
|
|
87
|
+
type: "Literal",
|
|
88
|
+
value: "\"" + str.split("").map(x => "\\x" + x.charCodeAt().toString(16)).join("") + "\""
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate URL-escaped string generator from string.
|
|
96
|
+
* @param {string} str
|
|
97
|
+
* @returns {Node}
|
|
98
|
+
*/
|
|
99
|
+
function makeStringUnescape(str) {
|
|
100
|
+
assert.equal(typeof str, "string");
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
type: "CallExpression",
|
|
104
|
+
callee: { type: "Identifier", name: "unescape" },
|
|
105
|
+
arguments: [
|
|
106
|
+
{
|
|
107
|
+
type: "Literal",
|
|
108
|
+
value: str.split("").map(x => "%" + x.charCodeAt().toString(16)).join("")
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate char-code-escaped char generator from char.
|
|
116
|
+
* @param {string} cha
|
|
117
|
+
* @returns {Node}
|
|
118
|
+
*/
|
|
119
|
+
function makeCharByte(cha) {
|
|
120
|
+
assert.equal(typeof cha, "string");
|
|
121
|
+
assert.equal(cha.length, 1);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: "CallExpression",
|
|
125
|
+
callee: {
|
|
126
|
+
type: "MemberExpression",
|
|
127
|
+
computed: false,
|
|
128
|
+
object: { type: "Identifier", name: "String" },
|
|
129
|
+
property: { type: "Identifier", name: "fromCharCode" }
|
|
130
|
+
},
|
|
131
|
+
arguments: [
|
|
132
|
+
{
|
|
133
|
+
type: "Literal",
|
|
134
|
+
value: cha.charCodeAt(0)
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate char-code-escaped string generator from string.
|
|
142
|
+
* @param {string} str
|
|
143
|
+
* @returns {Node}
|
|
144
|
+
*/
|
|
145
|
+
function makeStringByteArrayCall(str) {
|
|
146
|
+
assert.equal(typeof str, "string");
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: "CallExpression",
|
|
150
|
+
callee: { type: "Identifier", name: "veilmark$fromCharCodes" },
|
|
151
|
+
arguments: str.split("").map(x => ({ type: "Literal", value: x.charCodeAt() }))
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = class Literals {
|
|
156
|
+
|
|
157
|
+
constructor (logger) {
|
|
158
|
+
this.logger = logger;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Move strings into $$strings array
|
|
163
|
+
* @param {Node} ast Root node
|
|
164
|
+
* @returns {Node} Root node
|
|
165
|
+
*/
|
|
166
|
+
extractStrings (ast) {
|
|
167
|
+
assert.ok(estest.isNode(ast));
|
|
168
|
+
|
|
169
|
+
var global = { type: "Identifier", name: "$$strings" };
|
|
170
|
+
|
|
171
|
+
var strings = [];
|
|
172
|
+
var stringMap = {};
|
|
173
|
+
|
|
174
|
+
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
175
|
+
if (node.type == "Literal" && typeof node.value == "string") {
|
|
176
|
+
var idx = stringMap["_" + node.value];
|
|
177
|
+
if (!idx) {
|
|
178
|
+
stringMap["_" + node.value] = idx = strings.length;
|
|
179
|
+
strings.push(node);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
type: "MemberExpression",
|
|
184
|
+
computed: true,
|
|
185
|
+
object: global,
|
|
186
|
+
property: { type: "Literal", value: idx }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return node;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
ast.body.splice(0, 0, {
|
|
194
|
+
type: "VariableDeclaration",
|
|
195
|
+
kind: "var",
|
|
196
|
+
declarations: [
|
|
197
|
+
{
|
|
198
|
+
type: "VariableDeclarator",
|
|
199
|
+
id: global,
|
|
200
|
+
init: {
|
|
201
|
+
type: "ArrayExpression",
|
|
202
|
+
elements: strings
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return ast;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Replace string literals with string generators.
|
|
213
|
+
* @param {Node} ast Root node
|
|
214
|
+
* @returns {Node} Root node
|
|
215
|
+
*/
|
|
216
|
+
generateStrings (ast) {
|
|
217
|
+
assert.ok(estest.isNode(ast));
|
|
218
|
+
|
|
219
|
+
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
220
|
+
if (node.type == "Literal"
|
|
221
|
+
&& typeof node.value == "string"
|
|
222
|
+
&& stack.length > 1
|
|
223
|
+
&& stack[1].node.type != "Property") {
|
|
224
|
+
return makeStringGenerator(node.value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return node;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return ast;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
};
|