UNPKG

6.71 kBJavaScriptView Raw
1"use strict";
2
3const assert = require("assert");
4const stringmap = require("stringmap");
5const stringset = require("stringset");
6const is = require("simple-is");
7const fmt = require("simple-fmt");
8const error = require("./error");
9const options = require("./options");
10
11function Scope(args) {
12 assert(is.someof(args.kind, ["hoist", "block", "catch-block"]));
13 assert(is.object(args.node));
14 assert(args.parent === null || is.object(args.parent));
15
16 // kind === "hoist": function scopes, program scope, injected globals
17 // kind === "block": ES6 block scopes
18 // kind === "catch-block": catch block scopes
19 this.kind = args.kind;
20
21 // the AST node the block corresponds to
22 this.node = args.node;
23
24 // parent scope
25 this.parent = args.parent;
26
27 // children scopes for easier traversal (populated internally)
28 this.children = [];
29
30 // scope declarations. decls[variable_name] = {
31 // kind: "fun" for functions,
32 // "param" for function parameters,
33 // "caught" for catch parameter
34 // "var",
35 // "const",
36 // "let"
37 // node: the AST node the declaration corresponds to
38 // from: source code index from which it is visible at earliest
39 // (only stored for "const", "let" [and "var"] nodes)
40 // }
41 this.decls = stringmap();
42
43 // names of all declarations within this scope that was ever written
44 // TODO move to decls.w?
45 // TODO create corresponding read?
46 this.written = stringset();
47
48 // names of all variables declared outside this hoist scope but
49 // referenced in this scope (immediately or in child).
50 // only stored on hoist scopes for efficiency
51 // (because we currently generate lots of empty block scopes)
52 this.propagates = (this.kind === "hoist" ? stringset() : null);
53
54 // scopes register themselves with their parents for easier traversal
55 if (this.parent) {
56 this.parent.children.push(this);
57 }
58}
59
60Scope.prototype.print = function(indent) {
61 indent = indent || 0;
62 const scope = this;
63 const names = this.decls.keys().map(function(name) {
64 return fmt("{0} [{1}]", name, scope.decls.get(name).kind);
65 }).join(", ");
66 const propagates = this.propagates ? this.propagates.items().join(", ") : "";
67 console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates));
68 this.children.forEach(function(c) {
69 c.print(indent + 2);
70 });
71};
72
73Scope.prototype.add = function(name, kind, node, referableFromPos) {
74 assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"]));
75
76 function isConstLet(kind) {
77 return is.someof(kind, ["const", "let"]);
78 }
79
80 let scope = this;
81
82 // search nearest hoist-scope for fun, param and var's
83 // const, let and caught variables go directly in the scope (which may be hoist, block or catch-block)
84 if (is.someof(kind, ["fun", "param", "var"])) {
85 while (scope.kind !== "hoist") {
86 if (scope.decls.has(name) && isConstLet(scope.decls.get(name).kind)) { // could be caught
87 return error(node.loc.start.line, "{0} is already declared", name);
88 }
89 scope = scope.parent;
90 }
91 }
92 // name exists in scope and either new or existing kind is const|let => error
93 if (scope.decls.has(name) && (options.disallowDuplicated || isConstLet(scope.decls.get(name).kind) || isConstLet(kind))) {
94 return error(node.loc.start.line, "{0} is already declared", name);
95 }
96
97 const declaration = {
98 kind: kind,
99 node: node,
100 };
101 if (referableFromPos) {
102 assert(is.someof(kind, ["var", "const", "let"]));
103 declaration.from = referableFromPos;
104 }
105 scope.decls.set(name, declaration);
106};
107
108Scope.prototype.getKind = function(name) {
109 assert(is.string(name));
110 const decl = this.decls.get(name);
111 return decl ? decl.kind : null;
112};
113
114Scope.prototype.getNode = function(name) {
115 assert(is.string(name));
116 const decl = this.decls.get(name);
117 return decl ? decl.node : null;
118};
119
120Scope.prototype.getFromPos = function(name) {
121 assert(is.string(name));
122 const decl = this.decls.get(name);
123 return decl ? decl.from : null;
124};
125
126Scope.prototype.hasOwn = function(name) {
127 return this.decls.has(name);
128};
129
130Scope.prototype.remove = function(name) {
131 return this.decls.delete(name);
132};
133
134Scope.prototype.doesPropagate = function(name) {
135 return this.propagates.has(name);
136};
137
138Scope.prototype.markPropagates = function(name) {
139 this.propagates.add(name);
140};
141
142Scope.prototype.closestHoistScope = function() {
143 let scope = this;
144 while (scope.kind !== "hoist") {
145 scope = scope.parent;
146 }
147 return scope;
148};
149
150Scope.prototype.hasFunctionScopeBetween = function(outer) {
151 function isFunction(node) {
152 return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]);
153 }
154
155 for (let scope = this; scope; scope = scope.parent) {
156 if (scope === outer) {
157 return false;
158 }
159 if (isFunction(scope.node)) {
160 return true;
161 }
162 }
163
164 throw new Error("wasn't inner scope of outer");
165};
166
167Scope.prototype.lookup = function(name) {
168 for (let scope = this; scope; scope = scope.parent) {
169 if (scope.decls.has(name)) {
170 return scope;
171 } else if (scope.kind === "hoist") {
172 scope.propagates.add(name);
173 }
174 }
175 return null;
176};
177
178Scope.prototype.markWrite = function(name) {
179 assert(is.string(name));
180 this.written.add(name);
181};
182
183// detects let variables that are never modified (ignores top-level)
184Scope.prototype.detectUnmodifiedLets = function() {
185 const outmost = this;
186
187 function detect(scope) {
188 if (scope !== outmost) {
189 scope.decls.keys().forEach(function(name) {
190 if (scope.getKind(name) === "let" && !scope.written.has(name)) {
191 return error(scope.getNode(name).loc.start.line, "{0} is declared as let but never modified so could be const", name);
192 }
193 });
194 }
195
196 scope.children.forEach(function(childScope) {
197 detect(childScope);;
198 });
199 }
200 detect(this);
201};
202
203Scope.prototype.traverse = function(options) {
204 options = options || {};
205 const pre = options.pre;
206 const post = options.post;
207
208 function visit(scope) {
209 if (pre) {
210 pre(scope);
211 }
212 scope.children.forEach(function(childScope) {
213 visit(childScope);
214 });
215 if (post) {
216 post(scope);
217 }
218 }
219
220 visit(this);
221};
222
223module.exports = Scope;