UNPKG

14.7 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 // textchange var x => var x$1
288 if (newName !== name) {
289 changes.push({
290 start: declarator.id.range[0],
291 end: declarator.id.range[1],
292 str: newName,
293 });
294 }
295 });
296 }
297 }
298
299 function renameReference(node) {
300 if (!node.$refToScope) {
301 return;
302 }
303 const move = node.$refToScope.getMove(node.name);
304 if (!move) {
305 return;
306 }
307 node.$refToScope = move.scope;
308
309 if (node.name !== move.name) {
310 node.name = move.name;
311 changes.push({
312 start: node.range[0],
313 end: node.range[1],
314 str: move.name,
315 });
316 }
317 }
318
319 traverse(ast, {pre: renameDeclaration});
320 traverse(ast, {pre: renameReference});
321
322 return alter(src, changes);
323}
324
325
326let outermostLoop = null;
327let functions = [];
328function detectLoopClosuresPre(node) {
329 if (outermostLoop === null && isLoop(node)) {
330 outermostLoop = node;
331 }
332 if (!outermostLoop) {
333 // not inside loop
334 return;
335 }
336
337 // collect function-chain (as long as we're inside a loop)
338 if (isFunction(node)) {
339 functions.push(node);
340 }
341 if (functions.length === 0) {
342 // not inside function
343 return;
344 }
345
346 if (isReference(node) && isConstLet(node.$refToScope.getKind(node.name))) {
347 let n = node.$refToScope.node;
348
349 // node is an identifier
350 // scope refers to the scope where the variable is defined
351 // loop ..-> function ..-> node
352
353 let ok = true;
354 while (n) {
355// n.print();
356// console.log("--");
357 if (n === functions[functions.length - 1]) {
358 // we're ok (function-local)
359 break;
360 }
361 if (n === outermostLoop) {
362 // not ok (between loop and function)
363 ok = false;
364 break;
365 }
366// console.log("# " + scope.node.type);
367 n = n.$parent;
368// console.log("# " + scope.node);
369 }
370 if (ok) {
371// console.log("ok loop + closure: " + node.name);
372 } else {
373 error(getline(node), "can't transform closure. {0} is defined outside closure, inside loop", node.name);
374 }
375
376
377 /*
378 walk the scopes, starting from innermostFunction, ending at outermostLoop
379 if the referenced scope is somewhere in-between, then we have an issue
380 if the referenced scope is inside innermostFunction, then no problem (function-local const|let)
381 if the referenced scope is outside outermostLoop, then no problem (const|let external to the loop)
382
383 */
384 }
385}
386
387function detectLoopClosuresPost(node) {
388 if (outermostLoop === node) {
389 outermostLoop = null;
390 }
391 if (isFunction(node)) {
392 functions.pop();
393 }
394}
395
396function detectConstAssignment(node) {
397 if (isLvalue(node)) {
398 const scope = node.$scope.lookup(node.name);
399 if (scope && scope.getKind(node.name) === "const") {
400 error(getline(node), "can't assign to const variable {0}", node.name);
401 }
402 }
403}
404
405function detectConstantLets(ast) {
406 traverse(ast, {pre: function(node) {
407 if (isLvalue(node)) {
408 const scope = node.$scope.lookup(node.name);
409 if (scope) {
410 scope.markWrite(node.name);
411 }
412 }
413 }});
414
415 ast.$scope.detectUnmodifiedLets();
416}
417
418
419function run(src, config) {
420 // alter the options singleton with user configuration
421 for (let key in config) {
422 options[key] = config[key];
423 }
424
425 const ast = esprima(src, {
426 loc: true,
427 range: true,
428 });
429
430 // TODO detect unused variables (never read)
431 allIdenfitiers = stringset();
432 error.reset();
433
434 traverse(ast, {pre: createScopes});
435 createTopScope(ast.$scope, options.environments, options.globals);
436 traverse(ast, {pre: setupReferences});
437 //ast.$scope.print(); process.exit(-1);
438 traverse(ast, {pre: detectLoopClosuresPre, post: detectLoopClosuresPost});
439 traverse(ast, {pre: detectConstAssignment});
440 //detectConstantLets(ast);
441 if (error.any) {
442 return {
443 exitcode: -1,
444 };
445 }
446
447 const transformedSrc = varify(ast, src);
448 return {
449 exitcode: 0,
450 src: transformedSrc,
451 };
452}
453
454module.exports = run;