1 | "use strict";
|
2 |
|
3 | const assert = require("assert");
|
4 | const is = require("simple-is");
|
5 | const fmt = require("simple-fmt");
|
6 | const stringmap = require("stringmap");
|
7 | const stringset = require("stringset");
|
8 | const alter = require("alter");
|
9 | const traverse = require("ast-traverse");
|
10 | const breakable = require("breakable");
|
11 | const Scope = require("./scope");
|
12 | const error = require("./error");
|
13 | const getline = error.getline;
|
14 | const options = require("./options");
|
15 | const Stats = require("./stats");
|
16 | const jshint_vars = require("./jshint_globals/vars.js");
|
17 |
|
18 |
|
19 | function isConstLet(kind) {
|
20 | return is.someof(kind, ["const", "let"]);
|
21 | }
|
22 |
|
23 | function isVarConstLet(kind) {
|
24 | return is.someof(kind, ["var", "const", "let"]);
|
25 | }
|
26 |
|
27 | function isNonFunctionBlock(node) {
|
28 | return node.type === "BlockStatement" && is.noneof(node.$parent.type, ["FunctionDeclaration", "FunctionExpression"]);
|
29 | }
|
30 |
|
31 | function isForWithConstLet(node) {
|
32 | return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
|
33 | }
|
34 |
|
35 | function isForInOfWithConstLet(node) {
|
36 | return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
|
37 | }
|
38 |
|
39 | function isForInOf(node) {
|
40 | return is.someof(node.type, ["ForInStatement", "ForOfStatement"]);
|
41 | }
|
42 |
|
43 | function isFunction(node) {
|
44 | return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]);
|
45 | }
|
46 |
|
47 | function isLoop(node) {
|
48 | return is.someof(node.type, ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]);
|
49 | }
|
50 |
|
51 | function isReference(node) {
|
52 | const parent = node.$parent;
|
53 | return node.$refToScope ||
|
54 | node.type === "Identifier" &&
|
55 | !(parent.type === "VariableDeclarator" && parent.id === node) &&
|
56 | !(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) &&
|
57 | !(parent.type === "Property" && parent.key === node) &&
|
58 | !(parent.type === "LabeledStatement" && parent.label === node) &&
|
59 | !(parent.type === "CatchClause" && parent.param === node) &&
|
60 | !(isFunction(parent) && parent.id === node) &&
|
61 | !(isFunction(parent) && is.someof(node, parent.params)) &&
|
62 | true;
|
63 | }
|
64 |
|
65 | function isLvalue(node) {
|
66 | return isReference(node) &&
|
67 | ((node.$parent.type === "AssignmentExpression" && node.$parent.left === node) ||
|
68 | (node.$parent.type === "UpdateExpression" && node.$parent.argument === node));
|
69 | }
|
70 |
|
71 | function createScopes(node, parent) {
|
72 | assert(!node.$scope);
|
73 |
|
74 | node.$parent = parent;
|
75 | node.$scope = node.$parent ? node.$parent.$scope : null;
|
76 |
|
77 | if (node.type === "Program") {
|
78 |
|
79 |
|
80 | node.$scope = new Scope({
|
81 | kind: "hoist",
|
82 | node: node,
|
83 | parent: null,
|
84 | });
|
85 |
|
86 | } else if (isFunction(node)) {
|
87 |
|
88 |
|
89 |
|
90 | node.$scope = new Scope({
|
91 | kind: "hoist",
|
92 | node: node,
|
93 | parent: node.$parent.$scope,
|
94 | });
|
95 |
|
96 |
|
97 | if (node.id) {
|
98 | assert(node.id.type === "Identifier");
|
99 |
|
100 | if (node.type === "FunctionDeclaration") {
|
101 |
|
102 | node.$parent.$scope.add(node.id.name, "fun", node.id, null);
|
103 | } else if (node.type === "FunctionExpression") {
|
104 |
|
105 | node.$scope.add(node.id.name, "fun", node.id, null);
|
106 | } else {
|
107 | assert(false);
|
108 | }
|
109 | }
|
110 |
|
111 | node.params.forEach(function(param) {
|
112 | node.$scope.add(param.name, "param", param, null);
|
113 | });
|
114 |
|
115 | } else if (node.type === "VariableDeclaration") {
|
116 |
|
117 | assert(isVarConstLet(node.kind));
|
118 | node.declarations.forEach(function(declarator) {
|
119 | assert(declarator.type === "VariableDeclarator");
|
120 | const name = declarator.id.name;
|
121 | if (options.disallowVars && node.kind === "var") {
|
122 | error(getline(declarator), "var {0} is not allowed (use let or const)", name);
|
123 | }
|
124 | node.$scope.add(name, node.kind, declarator.id, declarator.range[1]);
|
125 | });
|
126 |
|
127 | } else if (isForWithConstLet(node) || isForInOfWithConstLet(node)) {
|
128 |
|
129 |
|
130 | node.$scope = new Scope({
|
131 | kind: "block",
|
132 | node: node,
|
133 | parent: node.$parent.$scope,
|
134 | });
|
135 |
|
136 | } else if (isNonFunctionBlock(node)) {
|
137 |
|
138 | node.$scope = new Scope({
|
139 | kind: "block",
|
140 | node: node,
|
141 | parent: node.$parent.$scope,
|
142 | });
|
143 |
|
144 | } else if (node.type === "CatchClause") {
|
145 | const identifier = node.param;
|
146 |
|
147 | node.$scope = new Scope({
|
148 | kind: "catch-block",
|
149 | node: node,
|
150 | parent: node.$parent.$scope,
|
151 | });
|
152 | node.$scope.add(identifier.name, "caught", identifier, null);
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | node.$scope.closestHoistScope().markPropagates(identifier.name);
|
164 | }
|
165 | }
|
166 |
|
167 | function createTopScope(programScope, environments, globals) {
|
168 | function inject(obj) {
|
169 | for (let name in obj) {
|
170 | const writeable = obj[name];
|
171 | const kind = (writeable ? "var" : "const");
|
172 | if (topScope.hasOwn(name)) {
|
173 | topScope.remove(name);
|
174 | }
|
175 | topScope.add(name, kind, {loc: {start: {line: -1}}}, -1);
|
176 | }
|
177 | }
|
178 |
|
179 | const topScope = new Scope({
|
180 | kind: "hoist",
|
181 | node: {},
|
182 | parent: null,
|
183 | });
|
184 |
|
185 | const complementary = {
|
186 | undefined: false,
|
187 | Infinity: false,
|
188 | console: false,
|
189 | };
|
190 |
|
191 | inject(complementary);
|
192 | inject(jshint_vars.reservedVars);
|
193 | inject(jshint_vars.ecmaIdentifiers);
|
194 | if (environments) {
|
195 | environments.forEach(function(env) {
|
196 | if (!jshint_vars[env]) {
|
197 | error(-1, 'environment "{0}" not found', env);
|
198 | } else {
|
199 | inject(jshint_vars[env]);
|
200 | }
|
201 | });
|
202 | }
|
203 | if (globals) {
|
204 | inject(globals);
|
205 | }
|
206 |
|
207 |
|
208 | programScope.parent = topScope;
|
209 | topScope.children.push(programScope);
|
210 |
|
211 | return topScope;
|
212 | }
|
213 |
|
214 | function setupReferences(ast, allIdentifiers, opts) {
|
215 | const analyze = (is.own(opts, "analyze") ? opts.analyze : true);
|
216 |
|
217 | function visit(node) {
|
218 | if (!isReference(node)) {
|
219 | return;
|
220 | }
|
221 | allIdentifiers.add(node.name);
|
222 |
|
223 | const scope = node.$scope.lookup(node.name);
|
224 | if (analyze && !scope && options.disallowUnknownReferences) {
|
225 | error(getline(node), "reference to unknown global variable {0}", node.name);
|
226 | }
|
227 |
|
228 | if (analyze && scope && is.someof(scope.getKind(node.name), ["const", "let"])) {
|
229 | const allowedFromPos = scope.getFromPos(node.name);
|
230 | const referencedAtPos = node.range[0];
|
231 | assert(is.finitenumber(allowedFromPos));
|
232 | assert(is.finitenumber(referencedAtPos));
|
233 | if (referencedAtPos < allowedFromPos) {
|
234 | if (!node.$scope.hasFunctionScopeBetween(scope)) {
|
235 | error(getline(node), "{0} is referenced before its declaration", node.name);
|
236 | }
|
237 | }
|
238 | }
|
239 | node.$refToScope = scope;
|
240 | }
|
241 |
|
242 | traverse(ast, {pre: visit});
|
243 | }
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | function varify(ast, stats, allIdentifiers, changes) {
|
249 | function unique(name) {
|
250 | assert(allIdentifiers.has(name));
|
251 | for (let cnt = 0; ; cnt++) {
|
252 | const genName = name + "$" + String(cnt);
|
253 | if (!allIdentifiers.has(genName)) {
|
254 | return genName;
|
255 | }
|
256 | }
|
257 | }
|
258 |
|
259 | function renameDeclarations(node) {
|
260 | if (node.type === "VariableDeclaration" && isConstLet(node.kind)) {
|
261 | const hoistScope = node.$scope.closestHoistScope();
|
262 | const origScope = node.$scope;
|
263 |
|
264 |
|
265 | changes.push({
|
266 | start: node.range[0],
|
267 | end: node.range[0] + node.kind.length,
|
268 | str: "var",
|
269 | });
|
270 |
|
271 | node.declarations.forEach(function(declarator) {
|
272 | assert(declarator.type === "VariableDeclarator");
|
273 | const name = declarator.id.name;
|
274 |
|
275 | stats.declarator(node.kind);
|
276 |
|
277 |
|
278 |
|
279 |
|
280 | const rename = (origScope !== hoistScope &&
|
281 | (hoistScope.hasOwn(name) || hoistScope.doesPropagate(name)));
|
282 |
|
283 | const newName = (rename ? unique(name) : name);
|
284 |
|
285 | origScope.remove(name);
|
286 | hoistScope.add(newName, "var", declarator.id, declarator.range[1]);
|
287 |
|
288 | origScope.moves = origScope.moves || stringmap();
|
289 | origScope.moves.set(name, {
|
290 | name: newName,
|
291 | scope: hoistScope,
|
292 | });
|
293 |
|
294 | allIdentifiers.add(newName);
|
295 |
|
296 | if (newName !== name) {
|
297 | stats.rename(name, newName, getline(declarator));
|
298 |
|
299 | declarator.id.originalName = name;
|
300 | declarator.id.name = newName;
|
301 |
|
302 |
|
303 | changes.push({
|
304 | start: declarator.id.range[0],
|
305 | end: declarator.id.range[1],
|
306 | str: newName,
|
307 | });
|
308 | }
|
309 | });
|
310 |
|
311 |
|
312 | node.kind = "var";
|
313 | }
|
314 | }
|
315 |
|
316 | function renameReferences(node) {
|
317 | if (!node.$refToScope) {
|
318 | return;
|
319 | }
|
320 | const move = node.$refToScope.moves && node.$refToScope.moves.get(node.name);
|
321 | if (!move) {
|
322 | return;
|
323 | }
|
324 | node.$refToScope = move.scope;
|
325 |
|
326 | if (node.name !== move.name) {
|
327 | node.originalName = node.name;
|
328 | node.name = move.name;
|
329 |
|
330 | if (node.alterop) {
|
331 |
|
332 | let existingOp = null;
|
333 | for (let i = 0; i < changes.length; i++) {
|
334 | const op = changes[i];
|
335 | if (op.node === node) {
|
336 | existingOp = op;
|
337 | break;
|
338 | }
|
339 | }
|
340 | assert(existingOp);
|
341 |
|
342 |
|
343 | existingOp.str = move.name;
|
344 | } else {
|
345 | changes.push({
|
346 | start: node.range[0],
|
347 | end: node.range[1],
|
348 | str: move.name,
|
349 | });
|
350 | }
|
351 | }
|
352 | }
|
353 |
|
354 | traverse(ast, {pre: renameDeclarations});
|
355 | traverse(ast, {pre: renameReferences});
|
356 | ast.$scope.traverse({pre: function(scope) {
|
357 | delete scope.moves;
|
358 | }});
|
359 | }
|
360 |
|
361 |
|
362 | function detectLoopClosures(ast) {
|
363 | traverse(ast, {pre: visit});
|
364 |
|
365 | function detectIifyBodyBlockers(body, node) {
|
366 | return breakable(function(brk) {
|
367 | traverse(body, {pre: function(n) {
|
368 |
|
369 | if (isFunction(n)) {
|
370 | return false;
|
371 | }
|
372 |
|
373 | let err = true;
|
374 | const msg = "loop-variable {0} is captured by a loop-closure that can't be transformed due to use of {1} at line {2}";
|
375 | if (n.type === "BreakStatement") {
|
376 | error(getline(node), msg, node.name, "break", getline(n));
|
377 | } else if (n.type === "ContinueStatement") {
|
378 | error(getline(node), msg, node.name, "continue", getline(n));
|
379 | } else if (n.type === "ReturnStatement") {
|
380 | error(getline(node), msg, node.name, "return", getline(n));
|
381 | } else if (n.type === "YieldExpression") {
|
382 | error(getline(node), msg, node.name, "yield", getline(n));
|
383 | } else if (n.type === "Identifier" && n.name === "arguments") {
|
384 | error(getline(node), msg, node.name, "arguments", getline(n));
|
385 | } else if (n.type === "VariableDeclaration" && n.kind === "var") {
|
386 | error(getline(node), msg, node.name, "var", getline(n));
|
387 | } else {
|
388 | err = false;
|
389 | }
|
390 | if (err) {
|
391 | brk(true);
|
392 | }
|
393 | }});
|
394 | return false;
|
395 | });
|
396 | }
|
397 |
|
398 | function visit(node) {
|
399 |
|
400 |
|
401 | var loopNode = null;
|
402 | if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) {
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | for (let n = node.$refToScope.node; ; ) {
|
408 | if (isFunction(n)) {
|
409 |
|
410 | return;
|
411 | } else if (isLoop(n)) {
|
412 | loopNode = n;
|
413 |
|
414 | break;
|
415 | }
|
416 | n = n.$parent;
|
417 | if (!n) {
|
418 |
|
419 | return;
|
420 | }
|
421 | }
|
422 |
|
423 | assert(isLoop(loopNode));
|
424 |
|
425 |
|
426 |
|
427 | const defScope = node.$refToScope;
|
428 | const generateIIFE = (options.loopClosures === "iife");
|
429 |
|
430 | for (let s = node.$scope; s; s = s.parent) {
|
431 | if (s === defScope) {
|
432 |
|
433 | return;
|
434 | } else if (isFunction(s.node)) {
|
435 |
|
436 |
|
437 |
|
438 | if (!generateIIFE) {
|
439 | const msg = "loop-variable {0} is captured by a loop-closure. Tried \"loopClosures\": \"iife\" in defs-config.json?";
|
440 | return error(getline(node), msg, node.name);
|
441 | }
|
442 |
|
443 |
|
444 |
|
445 |
|
446 | if (loopNode.type === "ForStatement" && defScope.node === loopNode) {
|
447 | const declarationNode = defScope.getNode(node.name);
|
448 | return error(getline(declarationNode), "Not yet specced ES6 feature. {0} is declared in for-loop header and then captured in loop closure", declarationNode.name);
|
449 | }
|
450 |
|
451 |
|
452 | if (detectIifyBodyBlockers(loopNode.body, node)) {
|
453 |
|
454 | return;
|
455 | }
|
456 |
|
457 |
|
458 | loopNode.$iify = true;
|
459 | }
|
460 | }
|
461 | }
|
462 | }
|
463 | }
|
464 |
|
465 | function transformLoopClosures(root, ops, options) {
|
466 | function insertOp(pos, str, node) {
|
467 | const op = {
|
468 | start: pos,
|
469 | end: pos,
|
470 | str: str,
|
471 | }
|
472 | if (node) {
|
473 | op.node = node;
|
474 | }
|
475 | ops.push(op);
|
476 | }
|
477 |
|
478 | traverse(root, {pre: function(node) {
|
479 | if (!node.$iify) {
|
480 | return;
|
481 | }
|
482 |
|
483 | const hasBlock = (node.body.type === "BlockStatement");
|
484 |
|
485 | const insertHead = (hasBlock ?
|
486 | node.body.range[0] + 1 :
|
487 | node.body.range[0]);
|
488 | const insertFoot = (hasBlock ?
|
489 | node.body.range[1] - 1 :
|
490 | node.body.range[1]);
|
491 |
|
492 | const forInName = (isForInOf(node) && node.left.declarations[0].id.name);;
|
493 | const iifeHead = fmt("(function({0}){", forInName ? forInName : "");
|
494 | const iifeTail = fmt("}).call(this{0});", forInName ? ", " + forInName : "");
|
495 |
|
496 |
|
497 | const iifeFragment = options.parse(iifeHead + iifeTail);
|
498 | const iifeExpressionStatement = iifeFragment.body[0];
|
499 | const iifeBlockStatement = iifeExpressionStatement.expression.callee.object.body;
|
500 |
|
501 | if (hasBlock) {
|
502 | const forBlockStatement = node.body;
|
503 | const tmp = forBlockStatement.body;
|
504 | forBlockStatement.body = [iifeExpressionStatement];
|
505 | iifeBlockStatement.body = tmp;
|
506 | } else {
|
507 | const tmp = node.body;
|
508 | node.body = iifeExpressionStatement;
|
509 | iifeBlockStatement.body[0] = tmp;
|
510 | }
|
511 |
|
512 |
|
513 | insertOp(insertHead, iifeHead);
|
514 |
|
515 | if (forInName) {
|
516 | insertOp(insertFoot, "}).call(this, ");
|
517 |
|
518 | const args = iifeExpressionStatement.expression.arguments;
|
519 | const iifeArgumentIdentifier = args[1];
|
520 | iifeArgumentIdentifier.alterop = true;
|
521 | insertOp(insertFoot, forInName, iifeArgumentIdentifier);
|
522 |
|
523 | insertOp(insertFoot, ");");
|
524 | } else {
|
525 | insertOp(insertFoot, iifeTail);
|
526 | }
|
527 | }});
|
528 | }
|
529 |
|
530 | function detectConstAssignment(ast) {
|
531 | traverse(ast, {pre: function(node) {
|
532 | if (isLvalue(node)) {
|
533 | const scope = node.$scope.lookup(node.name);
|
534 | if (scope && scope.getKind(node.name) === "const") {
|
535 | error(getline(node), "can't assign to const variable {0}", node.name);
|
536 | }
|
537 | }
|
538 | }});
|
539 | }
|
540 |
|
541 | function detectConstantLets(ast) {
|
542 | traverse(ast, {pre: function(node) {
|
543 | if (isLvalue(node)) {
|
544 | const scope = node.$scope.lookup(node.name);
|
545 | if (scope) {
|
546 | scope.markWrite(node.name);
|
547 | }
|
548 | }
|
549 | }});
|
550 |
|
551 | ast.$scope.detectUnmodifiedLets();
|
552 | }
|
553 |
|
554 | function setupScopeAndReferences(root, opts) {
|
555 |
|
556 | traverse(root, {pre: createScopes});
|
557 | const topScope = createTopScope(root.$scope, options.environments, options.globals);
|
558 |
|
559 |
|
560 |
|
561 | const allIdentifiers = stringset();
|
562 | topScope.traverse({pre: function(scope) {
|
563 | allIdentifiers.addMany(scope.decls.keys());
|
564 | }});
|
565 |
|
566 |
|
567 |
|
568 | setupReferences(root, allIdentifiers, opts);
|
569 | return allIdentifiers;
|
570 | }
|
571 |
|
572 | function cleanupTree(root) {
|
573 | traverse(root, {pre: function(node) {
|
574 | for (let prop in node) {
|
575 | if (prop[0] === "$") {
|
576 | delete node[prop];
|
577 | }
|
578 | }
|
579 | }});
|
580 | }
|
581 |
|
582 | function run(src, config) {
|
583 |
|
584 | for (let key in config) {
|
585 | options[key] = config[key];
|
586 | }
|
587 |
|
588 | let parsed;
|
589 |
|
590 | if (is.object(src)) {
|
591 | if (!options.ast) {
|
592 | return {
|
593 | errors: [
|
594 | "Can't produce string output when input is an AST. " +
|
595 | "Did you forget to set options.ast = true?"
|
596 | ],
|
597 | };
|
598 | }
|
599 |
|
600 |
|
601 | parsed = src;
|
602 |
|
603 | } else if (is.string(src)) {
|
604 | try {
|
605 | parsed = options.parse(src, {
|
606 | loc: true,
|
607 | range: true,
|
608 | });
|
609 | } catch (e) {
|
610 | return {
|
611 | errors: [
|
612 | fmt("line {0} column {1}: Error during input file parsing\n{2}\n{3}",
|
613 | e.lineNumber,
|
614 | e.column,
|
615 | src.split("\n")[e.lineNumber - 1],
|
616 | fmt.repeat(" ", e.column - 1) + "^")
|
617 | ],
|
618 | };
|
619 | }
|
620 |
|
621 | } else {
|
622 | return {
|
623 | errors: ["Input was neither an AST object nor a string."],
|
624 | };
|
625 | }
|
626 |
|
627 | const ast = parsed;
|
628 |
|
629 |
|
630 | error.reset();
|
631 |
|
632 | let allIdentifiers = setupScopeAndReferences(ast, {});
|
633 |
|
634 |
|
635 | detectLoopClosures(ast);
|
636 | detectConstAssignment(ast);
|
637 |
|
638 |
|
639 | const changes = [];
|
640 | transformLoopClosures(ast, changes, options);
|
641 |
|
642 |
|
643 |
|
644 | if (error.errors.length >= 1) {
|
645 | return {
|
646 | errors: error.errors,
|
647 | };
|
648 | }
|
649 |
|
650 | if (changes.length > 0) {
|
651 | cleanupTree(ast);
|
652 | allIdentifiers = setupScopeAndReferences(ast, {analyze: false});
|
653 | }
|
654 | assert(error.errors.length === 0);
|
655 |
|
656 |
|
657 |
|
658 |
|
659 | const stats = new Stats();
|
660 | varify(ast, stats, allIdentifiers, changes);
|
661 |
|
662 | if (options.ast) {
|
663 |
|
664 |
|
665 | cleanupTree(ast);
|
666 | return {
|
667 | stats: stats,
|
668 | ast: ast,
|
669 | };
|
670 | } else {
|
671 |
|
672 | const transformedSrc = alter(src, changes);
|
673 | return {
|
674 | stats: stats,
|
675 | src: transformedSrc,
|
676 | };
|
677 | }
|
678 | }
|
679 |
|
680 | module.exports = run;
|