UNPKG

16 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 // Function name goes in parent scope
89 if (node.id) {
90// if (node.type === "FunctionExpression") {
91// console.dir(node.id);
92// }
93// assert(node.type === "FunctionDeclaration"); // no support for named function expressions yet
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 // Variable declarations names goes in current scope
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 // For(In) loop with const|let declaration is a scope, with declaration in it
123 // There may be a block-scope under it
124 node.$scope = new Scope({
125 kind: "block",
126 node: node,
127 parent: node.$parent.$scope,
128 });
129
130 } else if (isNonFunctionBlock(node)) {
131 // A block node is a scope unless parent is a function
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 // All hoist-scope keeps track of which variables that are propagated through,
149 // i.e. an reference inside the scope points to a declaration outside the scope.
150 // This is used to mark "taint" the name since adding a new variable in the scope,
151 // with a propagated name, would change the meaning of the existing references.
152 //
153 // catch(e) is special because even though e is a variable in its own scope,
154 // we want to make sure that catch(e){let e} is never transformed to
155 // catch(e){var e} (but rather var e$0). For that reason we taint the use of e
156 // in the closest hoist-scope, i.e. where var e$0 belongs.
157 node.$scope.closestHoistScope().markPropagates(identifier.name);
158 }
159}
160
161function 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 // link it in
202 programScope.parent = topScope;
203 topScope.children.push(programScope);
204
205 return topScope;
206}
207
208function 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 // check const and let for referenced-before-declaration
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// TODO for loops init and body props are parallel to each other but init scope is outer that of body
238// TODO is this a problem?
239
240function 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 // text change const|let => var
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 // rename if
272 // 1) name already exists in hoistScope, or
273 // 2) name is already propagated (passed) through hoistScope or manually tainted
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 // textchange var x => var x$1
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
339let outermostLoop = null;
340let functions = [];
341function detectLoopClosuresPre(node) {
342 if (outermostLoop === null && isLoop(node)) {
343 outermostLoop = node;
344 }
345 if (!outermostLoop) {
346 // not inside loop
347 return;
348 }
349
350 // collect function-chain (as long as we're inside a loop)
351 if (isFunction(node)) {
352 functions.push(node);
353 }
354 if (functions.length === 0) {
355 // not inside function
356 return;
357 }
358
359 if (isReference(node) && isConstLet(node.$refToScope.getKind(node.name))) {
360 let n = node.$refToScope.node;
361
362 // node is an identifier
363 // scope refers to the scope where the variable is defined
364 // loop ..-> function ..-> node
365
366 let ok = true;
367 while (n) {
368// n.print();
369// console.log("--");
370 if (n === functions[functions.length - 1]) {
371 // we're ok (function-local)
372 break;
373 }
374 if (n === outermostLoop) {
375 // not ok (between loop and function)
376 ok = false;
377 break;
378 }
379// console.log("# " + scope.node.type);
380 n = n.$parent;
381// console.log("# " + scope.node);
382 }
383 if (ok) {
384// console.log("ok loop + closure: " + node.name);
385 } else {
386 error(getline(node), "can't transform closure. {0} is defined outside closure, inside loop", node.name);
387 }
388
389
390 /*
391 walk the scopes, starting from innermostFunction, ending at outermostLoop
392 if the referenced scope is somewhere in-between, then we have an issue
393 if the referenced scope is inside innermostFunction, then no problem (function-local const|let)
394 if the referenced scope is outside outermostLoop, then no problem (const|let external to the loop)
395
396 */
397 }
398}
399
400function detectLoopClosuresPost(node) {
401 if (outermostLoop === node) {
402 outermostLoop = null;
403 }
404 if (isFunction(node)) {
405 functions.pop();
406 }
407}
408
409function 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
418function 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
431function run(src, config) {
432 // alter the options singleton with user configuration
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 // TODO detect unused variables (never read)
443 error.reset();
444
445 // setup scopes
446 traverse(ast, {pre: createScopes});
447 const topScope = createTopScope(ast.$scope, options.environments, options.globals);
448
449 // allIdentifiers contains all declared and referenced vars
450 // collect all declaration names (including those in topScope)
451 const allIdentifiers = stringset();
452 topScope.traverse({pre: function(scope) {
453 allIdentifiers.addMany(scope.decls.keys());
454 }});
455
456 // setup node.$refToScope, check for errors.
457 // also collects all referenced names to allIdentifiers
458 setupReferences(ast, allIdentifiers);
459
460 // static analysis passes
461 traverse(ast, {pre: detectLoopClosuresPre, post: detectLoopClosuresPost});
462 traverse(ast, {pre: detectConstAssignment});
463 //detectConstantLets(ast);
464
465 //ast.$scope.print(); process.exit(-1);
466
467 if (error.errors.length >= 1) {
468 return {
469 errors: error.errors,
470 };
471 }
472
473 // change constlet declarations to var, renamed if needed
474 // varify modifies the scopes and AST accordingly and
475 // returns a list of change fragments (to use with alter)
476 const stats = new Stats();
477 const changes = varify(ast, stats, allIdentifiers);
478
479 if (options.ast) {
480 // return the modified AST instead of src code
481 // get rid of all added $ properties first, such as $parent and $scope
482 traverse(ast, {cleanup: true});
483 return {
484 stats: stats,
485 ast: ast,
486 };
487 } else {
488 // apply changes produced by varify and return the transformed src
489 const transformedSrc = alter(src, changes);
490 return {
491 stats: stats,
492 src: transformedSrc,
493 };
494 }
495}
496
497module.exports = run;