@dacely/toildefender 0.1.0
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 -0
- package/NOTICE.md +16 -0
- package/README.md +380 -0
- package/cli.js +168 -0
- package/defendjs.js +7 -0
- package/docs/all-modes-output.demo.js +673 -0
- package/estest.js +49 -0
- package/esutils.js +107 -0
- package/logger.js +28 -0
- package/obfuscator.js +534 -0
- package/package.json +108 -0
- package/processors/deadCode.js +62 -0
- package/processors/flattener.js +808 -0
- package/processors/health.js +55 -0
- package/processors/identifiers.js +256 -0
- package/processors/literalObfuscator.js +40 -0
- package/processors/literals.js +233 -0
- package/processors/methods.js +332 -0
- package/processors/modules.js +231 -0
- package/processors/normalizer.js +490 -0
- package/processors/numericVm.js +950 -0
- package/processors/postprocessing.js +69 -0
- package/processors/preprocessing.js +248 -0
- package/processors/scopes.js +177 -0
- package/processors/uglifier.js +26 -0
- package/processors/variables.js +185 -0
- package/toildefender.js +7 -0
- package/traverser.js +115 -0
- package/utils.js +135 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var assert = require("assert");
|
|
4
|
+
|
|
5
|
+
var _ = require("lodash");
|
|
6
|
+
var escodegen = require("escodegen");
|
|
7
|
+
|
|
8
|
+
var estest = require("../estest");
|
|
9
|
+
var traverser = require("../traverser");
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
module.exports = class Health {
|
|
13
|
+
|
|
14
|
+
constructor (logger) {
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.strict = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throwError (msg) {
|
|
20
|
+
if (this.strict) {
|
|
21
|
+
throw new Error(msg);
|
|
22
|
+
} else {
|
|
23
|
+
this.logger.warn(msg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Perform various health checks on the AST without modifying it.
|
|
29
|
+
* @param {Node} ast Root node
|
|
30
|
+
* @returns {Node} Root node
|
|
31
|
+
*/
|
|
32
|
+
check (ast) {
|
|
33
|
+
var visited = [];
|
|
34
|
+
|
|
35
|
+
traverser.traverse(ast, [], (node, stack) => {
|
|
36
|
+
if (_.includes(visited, node)) {
|
|
37
|
+
this.throwError("Node has multiple parents: " + JSON.stringify(node));
|
|
38
|
+
} else {
|
|
39
|
+
visited.push(node);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node.type == "BlockStatement") {
|
|
43
|
+
node.body.forEach(stmt => {
|
|
44
|
+
if (!estest.isStatement(stmt)) {
|
|
45
|
+
this.throwError(JSON.stringify(stack[1], null, 2));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return node;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return ast;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var assert = require("assert");
|
|
4
|
+
|
|
5
|
+
var _ = require("lodash");
|
|
6
|
+
|
|
7
|
+
var estest = require("../estest");
|
|
8
|
+
var ESUtils = require("../esutils");
|
|
9
|
+
var traverser = require("../traverser");
|
|
10
|
+
var utils = require("../utils");
|
|
11
|
+
|
|
12
|
+
function literal(value) {
|
|
13
|
+
return { type: "Literal", value: value };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function encodeObjectKey(key, salt, index) {
|
|
17
|
+
var encoded = [ key.length ^ ((salt + index * 131) & 65535) ];
|
|
18
|
+
for (var i = 0; i < key.length; i += 1) {
|
|
19
|
+
encoded.push(key.charCodeAt(i) ^ ((salt + index * 257 + i * 17) & 65535));
|
|
20
|
+
}
|
|
21
|
+
return encoded;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function objectKey(prop) {
|
|
25
|
+
return prop.key.name || prop.key.value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBigIntLiteral(node) {
|
|
29
|
+
return node.type == "Literal" && typeof node.value == "bigint";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = class Identifiers {
|
|
33
|
+
|
|
34
|
+
constructor (logger) {
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
this.esutils = new ESUtils(logger);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* This checks whether the given node has a parent that
|
|
41
|
+
* accepts undefined children without throwing errors.
|
|
42
|
+
* Those cannot be moved to separate variables without
|
|
43
|
+
* causing errors by assigning undefined variables
|
|
44
|
+
* to new variables.
|
|
45
|
+
* @param {Node} node
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
hasParentAcceptingUndefined (node) {
|
|
49
|
+
var parent = this.esutils.getParent(node);
|
|
50
|
+
return parent
|
|
51
|
+
&& parent.type == "UnaryExpression"
|
|
52
|
+
&& _.includes([ "typeof", "delete" ], parent.operator);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Replace property references like obj.prop with obj["prop"].
|
|
57
|
+
* @param {Node} ast Root node
|
|
58
|
+
* @returns {Node} Root node
|
|
59
|
+
*/
|
|
60
|
+
computeProperties (ast) {
|
|
61
|
+
assert.ok(estest.isNode(ast));
|
|
62
|
+
|
|
63
|
+
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
64
|
+
if (node.type == "MemberExpression"
|
|
65
|
+
&& !node.computed) {
|
|
66
|
+
assert(node.property.type == "Identifier");
|
|
67
|
+
node.property = { type: "Literal", value: node.property.name };
|
|
68
|
+
node.computed = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return node;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return ast;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Replace objects with an array via veilmark$toObject.
|
|
79
|
+
* @param {Node} ast Root node
|
|
80
|
+
* @returns {Node} Root node
|
|
81
|
+
*/
|
|
82
|
+
arrayizeObjects (ast, options) {
|
|
83
|
+
assert.ok(estest.isNode(ast));
|
|
84
|
+
options = options || {};
|
|
85
|
+
|
|
86
|
+
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
87
|
+
if (node.type == "ObjectExpression") {
|
|
88
|
+
if (options.objectPacking === false) {
|
|
89
|
+
var arr = [];
|
|
90
|
+
node.properties.forEach(prop => {
|
|
91
|
+
arr.push(literal(objectKey(prop)));
|
|
92
|
+
arr.push(prop.value);
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
type: "CallExpression",
|
|
96
|
+
callee: { type: "Identifier", name: "veilmark$toObject" },
|
|
97
|
+
arguments: [
|
|
98
|
+
{
|
|
99
|
+
type: "ArrayExpression",
|
|
100
|
+
elements: arr
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var salt = utils.random(1, 65535);
|
|
107
|
+
var schema = [ salt, node.properties.length ];
|
|
108
|
+
var values = [];
|
|
109
|
+
|
|
110
|
+
node.properties.forEach(prop => {
|
|
111
|
+
var key = objectKey(prop);
|
|
112
|
+
encodeObjectKey(String(key), salt, values.length).forEach(value => schema.push(value));
|
|
113
|
+
values.push(prop.value);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
type: "CallExpression",
|
|
118
|
+
callee: { type: "Identifier", name: "veilmark$toObject" },
|
|
119
|
+
arguments: [
|
|
120
|
+
{
|
|
121
|
+
type: "ArrayExpression",
|
|
122
|
+
elements: schema.map(literal)
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: "ArrayExpression",
|
|
126
|
+
elements: values
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return node;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return ast;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// This seems to be ununsed.
|
|
139
|
+
// TODO: Figure this out
|
|
140
|
+
moveIdentifiers (ast, scopeManager) {
|
|
141
|
+
assert.ok(estest.isNode(ast));
|
|
142
|
+
|
|
143
|
+
var rng = new utils.UniqueRandomAlpha(3);
|
|
144
|
+
|
|
145
|
+
this.esutils.setParentsRecursive(ast);
|
|
146
|
+
|
|
147
|
+
scopeManager.scopes.forEach(scope => {
|
|
148
|
+
/**
|
|
149
|
+
* That could cause problems if there are multiple unresolved
|
|
150
|
+
* references with the same name. (is that even possible?)
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
var replaced = new utils.HashMap();
|
|
154
|
+
|
|
155
|
+
scope.references
|
|
156
|
+
.filter(reference => !utils.isResolvedReference(reference))
|
|
157
|
+
.forEach(reference => {
|
|
158
|
+
if (replaced.exists(reference.identifier.name)) {
|
|
159
|
+
reference.identifier.name = replaced.get(reference.identifier.name);
|
|
160
|
+
} else if (!this.hasParentAcceptingUndefined(reference.identifier)) {
|
|
161
|
+
var name = "$$ident$" + rng.get();
|
|
162
|
+
replaced.set(reference.identifier.name, name);
|
|
163
|
+
|
|
164
|
+
var init;
|
|
165
|
+
if (reference.identifier.name == "undefined") {
|
|
166
|
+
init = { type: "Identifier", name: "undefined" };
|
|
167
|
+
} else {
|
|
168
|
+
init = {
|
|
169
|
+
type: "ConditionalExpression",
|
|
170
|
+
test: {
|
|
171
|
+
type: "BinaryExpression",
|
|
172
|
+
operator: "!==",
|
|
173
|
+
left: {
|
|
174
|
+
type: "UnaryExpression",
|
|
175
|
+
operator: "typeof",
|
|
176
|
+
prefix: true,
|
|
177
|
+
argument: { type: "Identifier", name: reference.identifier.name }
|
|
178
|
+
},
|
|
179
|
+
right: { type: "Literal", value: "undefined" }
|
|
180
|
+
},
|
|
181
|
+
consequent: { type: "Identifier", name: reference.identifier.name },
|
|
182
|
+
alternate: { type: "Identifier", name: "undefined" }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.esutils.insertIntoScope(scope, {
|
|
187
|
+
type: "VariableDeclaration",
|
|
188
|
+
kind: "var",
|
|
189
|
+
declarations: [
|
|
190
|
+
{
|
|
191
|
+
type: "VariableDeclarator",
|
|
192
|
+
id: { type: "Identifier", name: name },
|
|
193
|
+
init: init
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
reference.identifier.name = name;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return ast;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Move all literals into the veilmark$literals array.
|
|
208
|
+
* @param {Node} ast Root node
|
|
209
|
+
* @param {ScopeManager} scopeManager Scope manager
|
|
210
|
+
* @returns {Node} Root node
|
|
211
|
+
*/
|
|
212
|
+
moveLiterals (ast, scopeManager) {
|
|
213
|
+
assert.ok(estest.isNode(ast));
|
|
214
|
+
|
|
215
|
+
var rng = new utils.UniqueRandomAlpha(3);
|
|
216
|
+
|
|
217
|
+
var vars = [];
|
|
218
|
+
|
|
219
|
+
ast = traverser.traverse(ast, [], (node, stack) => {
|
|
220
|
+
if (node.type == "Literal" && !isBigIntLiteral(node) && stack.length > 0 && stack[1].node.type != "Property") {
|
|
221
|
+
var idx = vars.indexOf(node.value);
|
|
222
|
+
if (idx == -1) {
|
|
223
|
+
idx = vars.length;
|
|
224
|
+
vars.push(node.value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
type: "MemberExpression",
|
|
229
|
+
object: { type: "Identifier", name: "veilmark$literals" },
|
|
230
|
+
property: { type: "Literal", value: idx },
|
|
231
|
+
computed: true
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return node;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
ast.body.splice(0, 0, {
|
|
239
|
+
type: "VariableDeclaration",
|
|
240
|
+
kind: "var",
|
|
241
|
+
declarations: [
|
|
242
|
+
{
|
|
243
|
+
type: "VariableDeclarator",
|
|
244
|
+
id: { type: "Identifier", name: "veilmark$literals" },
|
|
245
|
+
init: {
|
|
246
|
+
type: "ArrayExpression",
|
|
247
|
+
elements: vars.map(x => ({ type: "Literal", value: x }))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return ast;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|