UNPKG

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