1 | "use strict";
|
2 |
|
3 | const esprima = require("esprima").parse;
|
4 | const assert = require("assert");
|
5 | const is = require("simple-is");
|
6 | const fmt = require("simple-fmt");
|
7 | const stringmap = require("stringmap");
|
8 | const stringset = require("stringset");
|
9 | const alter = require("alter");
|
10 | const traverse = require("./traverse");
|
11 | const Scope = require("./scope");
|
12 | const error = require("./error");
|
13 | const options = require("./options");
|
14 | const Stats = require("./stats");
|
15 | const jshint_vars = require("./jshint_globals/vars.js");
|
16 |
|
17 |
|
18 | function getline(node) {
|
19 | return node.loc.start.line;
|
20 | }
|
21 |
|
22 | function isConstLet(kind) {
|
23 | return is.someof(kind, ["const", "let"]);
|
24 | }
|
25 |
|
26 | function isVarConstLet(kind) {
|
27 | return is.someof(kind, ["var", "const", "let"]);
|
28 | }
|
29 |
|
30 | function isNonFunctionBlock(node) {
|
31 | return node.type === "BlockStatement" && is.noneof(node.$parent.type, ["FunctionDeclaration", "FunctionExpression"]);
|
32 | }
|
33 |
|
34 | function isForWithConstLet(node) {
|
35 | return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
|
36 | }
|
37 |
|
38 | function isForInWithConstLet(node) {
|
39 | return node.type === "ForInStatement" && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
|
40 | }
|
41 |
|
42 | function isFunction(node) {
|
43 | return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]);
|
44 | }
|
45 |
|
46 | function isLoop(node) {
|
47 | return is.someof(node.type, ["ForStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"]);
|
48 | }
|
49 |
|
50 | function isReference(node) {
|
51 | const parent = node.$parent;
|
52 | return node.$refToScope ||
|
53 | node.type === "Identifier" &&
|
54 | !(parent.type === "VariableDeclarator" && parent.id === node) &&
|
55 | !(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) &&
|
56 | !(parent.type === "Property" && parent.key === node) &&
|
57 | !(parent.type === "LabeledStatement" && parent.label === node) &&
|
58 | !(parent.type === "CatchClause" && parent.param === node) &&
|
59 | !(isFunction(parent) && parent.id === node) &&
|
60 | !(isFunction(parent) && is.someof(node, parent.params)) &&
|
61 | true;
|
62 | }
|
63 |
|
64 | function isLvalue(node) {
|
65 | return isReference(node) &&
|
66 | ((node.$parent.type === "AssignmentExpression" && node.$parent.left === node) ||
|
67 | (node.$parent.type === "UpdateExpression" && node.$parent.argument === node));
|
68 | }
|
69 |
|
70 | function createScopes(node, parent) {
|
71 | assert(!node.$scope);
|
72 |
|
73 | node.$parent = parent;
|
74 | node.$scope = node.$parent ? node.$parent.$scope : null;
|
75 |
|
76 | if (node.type === "Program") {
|
77 |
|
78 |
|
79 | node.$scope = new Scope({
|
80 | kind: "hoist",
|
81 | node: node,
|
82 | parent: null,
|
83 | });
|
84 |
|
85 | } else if (isFunction(node)) {
|
86 |
|
87 |
|
88 |
|
89 | if (node.id) {
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | assert(node.id.type === "Identifier");
|
96 | node.$parent.$scope.add(node.id.name, "fun", node.id, null);
|
97 | }
|
98 |
|
99 | node.$scope = new Scope({
|
100 | kind: "hoist",
|
101 | node: node,
|
102 | parent: node.$parent.$scope,
|
103 | });
|
104 |
|
105 | node.params.forEach(function(param) {
|
106 | node.$scope.add(param.name, "param", param, null);
|
107 | });
|
108 |
|
109 | } else if (node.type === "VariableDeclaration") {
|
110 |
|
111 | assert(isVarConstLet(node.kind));
|
112 | node.declarations.forEach(function(declarator) {
|
113 | assert(declarator.type === "VariableDeclarator");
|
114 | const name = declarator.id.name;
|
115 | if (options.disallowVars && node.kind === "var") {
|
116 | error(getline(declarator), "var {0} is not allowed (use let or const)", name);
|
117 | }
|
118 | node.$scope.add(name, node.kind, declarator.id, declarator.range[1]);
|
119 | });
|
120 |
|
121 | } else if (isForWithConstLet(node) || isForInWithConstLet(node)) {
|
122 |
|
123 |
|
124 | node.$scope = new Scope({
|
125 | kind: "block",
|
126 | node: node,
|
127 | parent: node.$parent.$scope,
|
128 | });
|
129 |
|
130 | } else if (isNonFunctionBlock(node)) {
|
131 |
|
132 | node.$scope = new Scope({
|
133 | kind: "block",
|
134 | node: node,
|
135 | parent: node.$parent.$scope,
|
136 | });
|
137 |
|
138 | } else if (node.type === "CatchClause") {
|
139 | const identifier = node.param;
|
140 |
|
141 | node.$scope = new Scope({
|
142 | kind: "catch-block",
|
143 | node: node,
|
144 | parent: node.$parent.$scope,
|
145 | });
|
146 | node.$scope.add(identifier.name, "caught", identifier, null);
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | node.$scope.closestHoistScope().markPropagates(identifier.name);
|
158 | }
|
159 | }
|
160 |
|
161 | function createTopScope(programScope, environments, globals) {
|
162 | function inject(obj) {
|
163 | for (let name in obj) {
|
164 | const writeable = obj[name];
|
165 | const kind = (writeable ? "var" : "const");
|
166 | if (topScope.hasOwn(name)) {
|
167 | topScope.remove(name);
|
168 | }
|
169 | topScope.add(name, kind, {loc: {start: {line: -1}}}, -1);
|
170 | }
|
171 | }
|
172 |
|
173 | const topScope = new Scope({
|
174 | kind: "hoist",
|
175 | node: {},
|
176 | parent: null,
|
177 | });
|
178 |
|
179 | const complementary = {
|
180 | undefined: false,
|
181 | Infinity: false,
|
182 | console: false,
|
183 | };
|
184 |
|
185 | inject(complementary);
|
186 | inject(jshint_vars.reservedVars);
|
187 | inject(jshint_vars.ecmaIdentifiers);
|
188 | if (environments) {
|
189 | environments.forEach(function(env) {
|
190 | if (!jshint_vars[env]) {
|
191 | error(-1, 'environment "{0}" not found', env);
|
192 | } else {
|
193 | inject(jshint_vars[env]);
|
194 | }
|
195 | });
|
196 | }
|
197 | if (globals) {
|
198 | inject(globals);
|
199 | }
|
200 |
|
201 |
|
202 | programScope.parent = topScope;
|
203 | topScope.children.push(programScope);
|
204 |
|
205 | return topScope;
|
206 | }
|
207 |
|
208 | function setupReferences(ast, allIdentifiers) {
|
209 | function visit(node) {
|
210 | if (!isReference(node)) {
|
211 | return;
|
212 | }
|
213 | allIdentifiers.add(node.name);
|
214 |
|
215 | const scope = node.$scope.lookup(node.name);
|
216 | if (!scope && options.disallowUnknownReferences) {
|
217 | error(getline(node), "reference to unknown global variable {0}", node.name);
|
218 | }
|
219 |
|
220 | if (scope && is.someof(scope.getKind(node.name), ["const", "let"])) {
|
221 | const allowedFromPos = scope.getFromPos(node.name);
|
222 | const referencedAtPos = node.range[0];
|
223 | assert(is.finitenumber(allowedFromPos));
|
224 | assert(is.finitenumber(referencedAtPos));
|
225 | if (referencedAtPos < allowedFromPos) {
|
226 | if (!node.$scope.hasFunctionScopeBetween(scope)) {
|
227 | error(getline(node), "{0} is referenced before its declaration", node.name);
|
228 | }
|
229 | }
|
230 | }
|
231 | node.$refToScope = scope;
|
232 | }
|
233 |
|
234 | traverse(ast, {pre: visit});
|
235 | }
|
236 |
|
237 |
|
238 |
|
239 |
|
240 | function varify(ast, stats, allIdentifiers) {
|
241 | const changes = [];
|
242 |
|
243 | function unique(name) {
|
244 | assert(allIdentifiers.has(name));
|
245 | for (let cnt = 0; ; cnt++) {
|
246 | const genName = name + "$" + String(cnt);
|
247 | if (!allIdentifiers.has(genName)) {
|
248 | return genName;
|
249 | }
|
250 | }
|
251 | }
|
252 |
|
253 | function renameDeclarations(node) {
|
254 | if (node.type === "VariableDeclaration" && isConstLet(node.kind)) {
|
255 | const hoistScope = node.$scope.closestHoistScope();
|
256 | const origScope = node.$scope;
|
257 |
|
258 |
|
259 | changes.push({
|
260 | start: node.range[0],
|
261 | end: node.range[0] + node.kind.length,
|
262 | str: "var",
|
263 | });
|
264 |
|
265 | node.declarations.forEach(function(declarator) {
|
266 | assert(declarator.type === "VariableDeclarator");
|
267 | const name = declarator.id.name;
|
268 |
|
269 | stats.declarator(node.kind);
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | const rename = (origScope !== hoistScope &&
|
275 | (hoistScope.hasOwn(name) || hoistScope.doesPropagate(name)));
|
276 |
|
277 | const newName = (rename ? unique(name) : name);
|
278 |
|
279 | origScope.remove(name);
|
280 | hoistScope.add(newName, "var", declarator.id, declarator.range[1]);
|
281 |
|
282 | origScope.moves = origScope.moves || stringmap();
|
283 | origScope.moves.set(name, {
|
284 | name: newName,
|
285 | scope: hoistScope,
|
286 | });
|
287 |
|
288 | allIdentifiers.add(newName);
|
289 |
|
290 | if (newName !== name) {
|
291 | stats.rename(name, newName, getline(declarator));
|
292 |
|
293 | declarator.id.originalName = name;
|
294 | declarator.id.name = newName;
|
295 |
|
296 |
|
297 | changes.push({
|
298 | start: declarator.id.range[0],
|
299 | end: declarator.id.range[1],
|
300 | str: newName,
|
301 | });
|
302 | }
|
303 | });
|
304 | }
|
305 | }
|
306 |
|
307 | function renameReferences(node) {
|
308 | if (!node.$refToScope) {
|
309 | return;
|
310 | }
|
311 | const move = node.$refToScope.moves && node.$refToScope.moves.get(node.name);
|
312 | if (!move) {
|
313 | return;
|
314 | }
|
315 | node.$refToScope = move.scope;
|
316 |
|
317 | if (node.name !== move.name) {
|
318 | node.originalName = node.name;
|
319 | node.name = move.name;
|
320 |
|
321 | changes.push({
|
322 | start: node.range[0],
|
323 | end: node.range[1],
|
324 | str: move.name,
|
325 | });
|
326 | }
|
327 | }
|
328 |
|
329 | traverse(ast, {pre: renameDeclarations});
|
330 | traverse(ast, {pre: renameReferences});
|
331 | ast.$scope.traverse({pre: function(scope) {
|
332 | delete scope.moves;
|
333 | }});
|
334 |
|
335 | return changes;
|
336 | }
|
337 |
|
338 |
|
339 | let outermostLoop = null;
|
340 | let functions = [];
|
341 | function detectLoopClosuresPre(node) {
|
342 | if (outermostLoop === null && isLoop(node)) {
|
343 | outermostLoop = node;
|
344 | }
|
345 | if (!outermostLoop) {
|
346 |
|
347 | return;
|
348 | }
|
349 |
|
350 |
|
351 | if (isFunction(node)) {
|
352 | functions.push(node);
|
353 | }
|
354 | if (functions.length === 0) {
|
355 |
|
356 | return;
|
357 | }
|
358 |
|
359 | if (isReference(node) && isConstLet(node.$refToScope.getKind(node.name))) {
|
360 | let n = node.$refToScope.node;
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 | let ok = true;
|
367 | while (n) {
|
368 |
|
369 |
|
370 | if (n === functions[functions.length - 1]) {
|
371 |
|
372 | break;
|
373 | }
|
374 | if (n === outermostLoop) {
|
375 |
|
376 | ok = false;
|
377 | break;
|
378 | }
|
379 |
|
380 | n = n.$parent;
|
381 |
|
382 | }
|
383 | if (ok) {
|
384 |
|
385 | } else {
|
386 | error(getline(node), "can't transform closure. {0} is defined outside closure, inside loop", node.name);
|
387 | }
|
388 |
|
389 |
|
390 | |
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 | }
|
398 | }
|
399 |
|
400 | function detectLoopClosuresPost(node) {
|
401 | if (outermostLoop === node) {
|
402 | outermostLoop = null;
|
403 | }
|
404 | if (isFunction(node)) {
|
405 | functions.pop();
|
406 | }
|
407 | }
|
408 |
|
409 | function detectConstAssignment(node) {
|
410 | if (isLvalue(node)) {
|
411 | const scope = node.$scope.lookup(node.name);
|
412 | if (scope && scope.getKind(node.name) === "const") {
|
413 | error(getline(node), "can't assign to const variable {0}", node.name);
|
414 | }
|
415 | }
|
416 | }
|
417 |
|
418 | function detectConstantLets(ast) {
|
419 | traverse(ast, {pre: function(node) {
|
420 | if (isLvalue(node)) {
|
421 | const scope = node.$scope.lookup(node.name);
|
422 | if (scope) {
|
423 | scope.markWrite(node.name);
|
424 | }
|
425 | }
|
426 | }});
|
427 |
|
428 | ast.$scope.detectUnmodifiedLets();
|
429 | }
|
430 |
|
431 | function run(src, config) {
|
432 |
|
433 | for (let key in config) {
|
434 | options[key] = config[key];
|
435 | }
|
436 |
|
437 | const ast = esprima(src, {
|
438 | loc: true,
|
439 | range: true,
|
440 | });
|
441 |
|
442 |
|
443 | error.reset();
|
444 |
|
445 |
|
446 | traverse(ast, {pre: createScopes});
|
447 | const topScope = createTopScope(ast.$scope, options.environments, options.globals);
|
448 |
|
449 |
|
450 |
|
451 | const allIdentifiers = stringset();
|
452 | topScope.traverse({pre: function(scope) {
|
453 | allIdentifiers.addMany(scope.decls.keys());
|
454 | }});
|
455 |
|
456 |
|
457 |
|
458 | setupReferences(ast, allIdentifiers);
|
459 |
|
460 |
|
461 | traverse(ast, {pre: detectLoopClosuresPre, post: detectLoopClosuresPost});
|
462 | traverse(ast, {pre: detectConstAssignment});
|
463 |
|
464 |
|
465 |
|
466 |
|
467 | if (error.errors.length >= 1) {
|
468 | return {
|
469 | errors: error.errors,
|
470 | };
|
471 | }
|
472 |
|
473 |
|
474 |
|
475 |
|
476 | const stats = new Stats();
|
477 | const changes = varify(ast, stats, allIdentifiers);
|
478 |
|
479 | if (options.ast) {
|
480 |
|
481 |
|
482 | traverse(ast, {cleanup: true});
|
483 | return {
|
484 | stats: stats,
|
485 | ast: ast,
|
486 | };
|
487 | } else {
|
488 |
|
489 | const transformedSrc = alter(src, changes);
|
490 | return {
|
491 | stats: stats,
|
492 | src: transformedSrc,
|
493 | };
|
494 | }
|
495 | }
|
496 |
|
497 | module.exports = run;
|