1 | "use strict";
|
2 |
|
3 | const assert = require("assert");
|
4 | const stringmap = require("stringmap");
|
5 | const stringset = require("stringset");
|
6 | const is = require("simple-is");
|
7 | const fmt = require("simple-fmt");
|
8 | const error = require("./error");
|
9 | const getline = error.getline;
|
10 | const options = require("./options");
|
11 |
|
12 | function 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 |
|
18 |
|
19 |
|
20 | this.kind = args.kind;
|
21 |
|
22 |
|
23 | this.node = args.node;
|
24 |
|
25 |
|
26 | this.parent = args.parent;
|
27 |
|
28 |
|
29 | this.children = [];
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | this.decls = stringmap();
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | this.written = stringset();
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | this.propagates = (this.kind === "hoist" ? stringset() : null);
|
54 |
|
55 |
|
56 | if (this.parent) {
|
57 | this.parent.children.push(this);
|
58 | }
|
59 | }
|
60 |
|
61 | Scope.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 |
|
74 | Scope.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 |
|
84 |
|
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)) {
|
88 | return error(getline(node), "{0} is already declared", name);
|
89 | }
|
90 | scope = scope.parent;
|
91 | }
|
92 | }
|
93 |
|
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 |
|
109 | Scope.prototype.getKind = function(name) {
|
110 | assert(is.string(name));
|
111 | const decl = this.decls.get(name);
|
112 | return decl ? decl.kind : null;
|
113 | };
|
114 |
|
115 | Scope.prototype.getNode = function(name) {
|
116 | assert(is.string(name));
|
117 | const decl = this.decls.get(name);
|
118 | return decl ? decl.node : null;
|
119 | };
|
120 |
|
121 | Scope.prototype.getFromPos = function(name) {
|
122 | assert(is.string(name));
|
123 | const decl = this.decls.get(name);
|
124 | return decl ? decl.from : null;
|
125 | };
|
126 |
|
127 | Scope.prototype.hasOwn = function(name) {
|
128 | return this.decls.has(name);
|
129 | };
|
130 |
|
131 | Scope.prototype.remove = function(name) {
|
132 | return this.decls.remove(name);
|
133 | };
|
134 |
|
135 | Scope.prototype.doesPropagate = function(name) {
|
136 | return this.propagates.has(name);
|
137 | };
|
138 |
|
139 | Scope.prototype.markPropagates = function(name) {
|
140 | this.propagates.add(name);
|
141 | };
|
142 |
|
143 | Scope.prototype.closestHoistScope = function() {
|
144 | let scope = this;
|
145 | while (scope.kind !== "hoist") {
|
146 | scope = scope.parent;
|
147 | }
|
148 | return scope;
|
149 | };
|
150 |
|
151 | Scope.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 |
|
168 | Scope.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 |
|
179 | Scope.prototype.markWrite = function(name) {
|
180 | assert(is.string(name));
|
181 | this.written.add(name);
|
182 | };
|
183 |
|
184 |
|
185 | Scope.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 |
|
204 | Scope.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 |
|
224 | module.exports = Scope;
|