UNPKG

5.64 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 this.kind = args.kind;
17 this.node = args.node;
18 this.parent = args.parent;
19 this.children = [];
20 this.decls = stringmap();
21 this.moves = stringmap();
22 this.written = stringset();
23 this.propagates = (this.kind === "hoist" ? stringset() : null);
24
25 if (this.parent) {
26 this.parent.children.push(this);
27 }
28}
29
30Scope.prototype.print = function(indent) {
31 indent = indent || 0;
32 const scope = this;
33 const names = this.decls.keys().map(function(name) {
34 return fmt("{0} [{1}]", name, scope.decls.get(name).kind);
35 }).join(", ");
36 const propagates = this.propagates ? this.propagates.items().join(", ") : "";
37 console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates));
38 this.children.forEach(function(c) {
39 c.print(indent + 2);
40 });
41};
42
43Scope.prototype.add = function(name, kind, node, referableFromPos) {
44 assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"]));
45
46 function isConstLet(kind) {
47 return is.someof(kind, ["const", "let"]);
48 }
49
50 let scope = this;
51
52 // search nearest hoist-scope for fun, param and var's
53 // const, let and caught variables go directly in the scope (which may be hoist, block or catch-block)
54 if (is.someof(kind, ["fun", "param", "var"])) {
55 while (scope.kind !== "hoist") {
56 if (scope.decls.has(name) && isConstLet(scope.decls.get(name).kind)) { // could be caught
57 return error(node.loc.start.line, "{0} is already declared", name);
58 }
59 scope = scope.parent;
60 }
61 }
62 // name exists in scope and either new or existing kind is const|let => error
63 if (scope.decls.has(name) && (options.disallowDuplicated || isConstLet(scope.decls.get(name).kind) || isConstLet(kind))) {
64 return error(node.loc.start.line, "{0} is already declared", name);
65 }
66
67 if (kind === "fun" && referableFromPos === null) {
68 referableFromPos = scope.node.range[0];
69 }
70
71 scope.decls.set(name, {
72 kind: kind,
73 node: node,
74 from: referableFromPos,
75 });
76};
77
78Scope.prototype.addGlobal = function(name, kind, node, referableFromPos) {
79 assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"]));
80 assert(this.parent === null);
81 this.decls.set(name, {
82 kind: kind,
83 node: node,
84 from: referableFromPos,
85 });
86};
87
88Scope.prototype.getKind = function(name) {
89 assert(is.string(name));
90 const decl = this.decls.get(name);
91 return decl ? decl.kind : null;
92};
93
94Scope.prototype.getNode = function(name) {
95 assert(is.string(name));
96 const decl = this.decls.get(name);
97 return decl ? decl.node : null;
98};
99
100Scope.prototype.getFromPos = function(name) {
101 assert(is.string(name));
102 const decl = this.decls.get(name);
103 return decl ? decl.from : null;
104};
105
106Scope.prototype.move = function(name, newName, newScope) {
107 assert(is.string(name));
108 assert(is.string(newName));
109 assert(this.decls.has(name));
110 this.decls.delete(name);
111 this.moves.set(name, {
112 name: newName,
113 scope: newScope,
114 });
115};
116
117Scope.prototype.getMove = function(name) {
118 return this.moves.get(name);
119};
120
121Scope.prototype.hasOwn = function(name) {
122 return this.decls.has(name);
123};
124
125Scope.prototype.doesPropagate = function(name) {
126 return this.propagates.has(name);
127};
128
129Scope.prototype.markPropagates = function(name) {
130 this.propagates.add(name);
131};
132
133Scope.prototype.closestHoistScope = function() {
134 let scope = this;
135 while (scope.kind !== "hoist") {
136 scope = scope.parent;
137 }
138 return scope;
139};
140
141Scope.prototype.hasFunctionScopeBetween = function(outer) {
142 function isFunction(node) {
143 return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]);
144 }
145
146 for (let scope = this; scope; scope = scope.parent) {
147 if (scope === outer) {
148 return false;
149 }
150 if (isFunction(scope.node)) {
151 return true;
152 }
153 }
154
155 throw new Error("wasn't inner scope of outer");
156};
157
158Scope.prototype.lookup = function(name) {
159 for (let scope = this; scope; scope = scope.parent) {
160 if (scope.decls.has(name)) {
161 return scope;
162 } else if (scope.kind === "hoist") {
163 scope.propagates.add(name);
164 }
165 }
166 return null;
167};
168
169Scope.prototype.markWrite = function(name) {
170 assert(is.string(name));
171 this.written.add(name);
172};
173
174// detects let variables that are never modified (ignores top-level)
175Scope.prototype.detectUnmodifiedLets = function() {
176 const outmost = this;
177
178 function detect(scope) {
179 if (scope !== outmost) {
180 scope.decls.keys().forEach(function(name) {
181 if (scope.getKind(name) === "let" && !scope.written.has(name)) {
182 return error(scope.getNode(name).loc.start.line, "{0} is declared as let but never modified so could be const", name);
183 }
184 });
185 }
186
187 scope.children.forEach(function(childScope) {
188 detect(childScope);;
189 });
190 }
191 detect(this);
192};
193
194module.exports = Scope;