UNPKG

23.3 kBJavaScriptView Raw
1"use strict";
2
3const assert = require("assert");
4const is = require("simple-is");
5const fmt = require("simple-fmt");
6const stringmap = require("stringmap");
7const stringset = require("stringset");
8const alter = require("alter");
9const traverse = require("ast-traverse");
10const breakable = require("breakable");
11const Scope = require("./scope");
12const error = require("./error");
13const getline = error.getline;
14const options = require("./options");
15const Stats = require("./stats");
16const jshint_vars = require("./jshint_globals/vars.js");
17
18
19function isConstLet(kind) {
20 return is.someof(kind, ["const", "let"]);
21}
22
23function isVarConstLet(kind) {
24 return is.someof(kind, ["var", "const", "let"]);
25}
26
27function isNonFunctionBlock(node) {
28 return node.type === "BlockStatement" && is.noneof(node.$parent.type, ["FunctionDeclaration", "FunctionExpression"]);
29}
30
31function isForWithConstLet(node) {
32 return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
33}
34
35function isForInOfWithConstLet(node) {
36 return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
37}
38
39function isForInOf(node) {
40 return is.someof(node.type, ["ForInStatement", "ForOfStatement"]);
41}
42
43function isFunction(node) {
44 return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]);
45}
46
47function isLoop(node) {
48 return is.someof(node.type, ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]);
49}
50
51function isReference(node) {
52 const parent = node.$parent;
53 return node.$refToScope ||
54 node.type === "Identifier" &&
55 !(parent.type === "VariableDeclarator" && parent.id === node) && // var|let|const $
56 !(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) && // obj.$
57 !(parent.type === "Property" && parent.key === node) && // {$: ...}
58 !(parent.type === "LabeledStatement" && parent.label === node) && // $: ...
59 !(parent.type === "CatchClause" && parent.param === node) && // catch($)
60 !(isFunction(parent) && parent.id === node) && // function $(..
61 !(isFunction(parent) && is.someof(node, parent.params)) && // function f($)..
62 true;
63}
64
65function 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
71function createScopes(node, parent) {
72 assert(!node.$scope);
73
74 node.$parent = parent;
75 node.$scope = node.$parent ? node.$parent.$scope : null; // may be overridden
76
77 if (node.type === "Program") {
78 // Top-level program is a scope
79 // There's no block-scope under it
80 node.$scope = new Scope({
81 kind: "hoist",
82 node: node,
83 parent: null,
84 });
85
86 } else if (isFunction(node)) {
87 // Function is a scope, with params in it
88 // There's no block-scope under it
89
90 node.$scope = new Scope({
91 kind: "hoist",
92 node: node,
93 parent: node.$parent.$scope,
94 });
95
96 // function has a name
97 if (node.id) {
98 assert(node.id.type === "Identifier");
99
100 if (node.type === "FunctionDeclaration") {
101 // Function name goes in parent scope for declared functions
102 node.$parent.$scope.add(node.id.name, "fun", node.id, null);
103 } else if (node.type === "FunctionExpression") {
104 // Function name goes in function's scope for named function expressions
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 // Variable declarations names goes in current scope
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 // For(In/Of) loop with const|let declaration is a scope, with declaration in it
129 // There may be a block-scope under it
130 node.$scope = new Scope({
131 kind: "block",
132 node: node,
133 parent: node.$parent.$scope,
134 });
135
136 } else if (isNonFunctionBlock(node)) {
137 // A block node is a scope unless parent is a function
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 // All hoist-scope keeps track of which variables that are propagated through,
155 // i.e. an reference inside the scope points to a declaration outside the scope.
156 // This is used to mark "taint" the name since adding a new variable in the scope,
157 // with a propagated name, would change the meaning of the existing references.
158 //
159 // catch(e) is special because even though e is a variable in its own scope,
160 // we want to make sure that catch(e){let e} is never transformed to
161 // catch(e){var e} (but rather var e$0). For that reason we taint the use of e
162 // in the closest hoist-scope, i.e. where var e$0 belongs.
163 node.$scope.closestHoistScope().markPropagates(identifier.name);
164 }
165}
166
167function 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 // link it in
208 programScope.parent = topScope;
209 topScope.children.push(programScope);
210
211 return topScope;
212}
213
214function 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 // check const and let for referenced-before-declaration
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// TODO for loops init and body props are parallel to each other but init scope is outer that of body
246// TODO is this a problem?
247
248function 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 // text change const|let => var
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 // rename if
278 // 1) name already exists in hoistScope, or
279 // 2) name is already propagated (passed) through hoistScope or manually tainted
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 // textchange var x => var x$1
303 changes.push({
304 start: declarator.id.range[0],
305 end: declarator.id.range[1],
306 str: newName,
307 });
308 }
309 });
310
311 // ast change const|let => var
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 // node has no range because it is the result of another alter operation
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 // modify op
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
362function 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 // if we hit an inner function of the loop body, don't traverse further
369 if (isFunction(n)) {
370 return false;
371 }
372
373 let err = true; // reset to false in else-statement below
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); // break traversal
392 }
393 }});
394 return false;
395 });
396 }
397
398 function visit(node) {
399 // forbidden pattern:
400 // <any>* <loop> <non-fn>* <constlet-def> <any>* <fn> <any>* <constlet-ref>
401 var loopNode = null;
402 if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) {
403 // traverse nodes up towards root from constlet-def
404 // if we hit a function (before a loop) - ok!
405 // if we hit a loop - maybe-ouch
406 // if we reach root - ok!
407 for (let n = node.$refToScope.node; ; ) {
408 if (isFunction(n)) {
409 // we're ok (function-local)
410 return;
411 } else if (isLoop(n)) {
412 loopNode = n;
413 // maybe not ok (between loop and function)
414 break;
415 }
416 n = n.$parent;
417 if (!n) {
418 // ok (reached root)
419 return;
420 }
421 }
422
423 assert(isLoop(loopNode));
424
425 // traverse scopes from reference-scope up towards definition-scope
426 // if we hit a function, ouch!
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 // we're ok
433 return;
434 } else if (isFunction(s.node)) {
435 // not ok (there's a function between the reference and definition)
436 // may be transformable via IIFE
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 // here be dragons
444 // for (let x = ..; .. ; ..) { (function(){x})() } is forbidden because of current
445 // spec and VM status
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 // speak now or forever hold your peace
452 if (detectIifyBodyBlockers(loopNode.body, node)) {
453 // error already generated
454 return;
455 }
456
457 // mark loop for IIFE-insertion
458 loopNode.$iify = true;
459 }
460 }
461 }
462 }
463}
464
465function 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 : // just after body {
487 node.body.range[0]); // just before existing expression
488 const insertFoot = (hasBlock ?
489 node.body.range[1] - 1 : // just before body }
490 node.body.range[1]); // just after existing expression
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 // modify AST
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 // create ops
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
530function 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
541function 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
554function setupScopeAndReferences(root, opts) {
555 // setup scopes
556 traverse(root, {pre: createScopes});
557 const topScope = createTopScope(root.$scope, options.environments, options.globals);
558
559 // allIdentifiers contains all declared and referenced vars
560 // collect all declaration names (including those in topScope)
561 const allIdentifiers = stringset();
562 topScope.traverse({pre: function(scope) {
563 allIdentifiers.addMany(scope.decls.keys());
564 }});
565
566 // setup node.$refToScope, check for errors.
567 // also collects all referenced names to allIdentifiers
568 setupReferences(root, allIdentifiers, opts);
569 return allIdentifiers;
570}
571
572function 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
582function run(src, config) {
583 // alter the options singleton with user configuration
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 // Received an AST object as src, so no need to parse it.
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 // TODO detect unused variables (never read)
630 error.reset();
631
632 let allIdentifiers = setupScopeAndReferences(ast, {});
633
634 // static analysis passes
635 detectLoopClosures(ast);
636 detectConstAssignment(ast);
637 //detectConstantLets(ast);
638
639 const changes = [];
640 transformLoopClosures(ast, changes, options);
641
642 //ast.$scope.print(); process.exit(-1);
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 // change constlet declarations to var, renamed if needed
657 // varify modifies the scopes and AST accordingly and
658 // returns a list of change fragments (to use with alter)
659 const stats = new Stats();
660 varify(ast, stats, allIdentifiers, changes);
661
662 if (options.ast) {
663 // return the modified AST instead of src code
664 // get rid of all added $ properties first, such as $parent and $scope
665 cleanupTree(ast);
666 return {
667 stats: stats,
668 ast: ast,
669 };
670 } else {
671 // apply changes produced by varify and return the transformed src
672 const transformedSrc = alter(src, changes);
673 return {
674 stats: stats,
675 src: transformedSrc,
676 };
677 }
678}
679
680module.exports = run;