UNPKG

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