UNPKG

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