-
Notifications
You must be signed in to change notification settings - Fork 150
/
scopetools.js
180 lines (151 loc) · 6.03 KB
/
scopetools.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// scopetools.js
// MIT licensed, see LICENSE file
// Copyright (c) 2013-2016 Olov Lassus <olov.lassus@gmail.com>
"use strict";
const assert = require("assert");
const traverse = require("ordered-ast-traverse");
const Scope = require("./scope");
const is = require("simple-is");
module.exports = {
setupScopeAndReferences: setupScopeAndReferences,
isReference: isReference,
};
function setupScopeAndReferences(root) {
traverse(root, {pre: createScopes});
createTopScope(root.$scope);
}
function createScopes(node, parent) {
node.$parent = parent;
node.$scope = parent ? parent.$scope : null; // may be overridden
if (isNonFunctionBlock(node, parent)) {
// A block node is a scope unless parent is a function
node.$scope = new Scope({
kind: "block",
node: node,
parent: parent.$scope,
});
} else if (node.type === "VariableDeclaration") {
// Variable declarations names goes in current scope
node.declarations.forEach(function(declarator) {
const name = declarator.id.name;
node.$scope.add(name, node.kind, declarator.id, declarator.range[1]);
});
} else if (isFunction(node)) {
// Function is a scope, with params in it
// There's no block-scope under it
node.$scope = new Scope({
kind: "hoist",
node: node,
parent: parent.$scope,
});
// function has a name
if (node.id) {
if (node.type === "FunctionDeclaration") {
// Function name goes in parent scope for declared functions
parent.$scope.add(node.id.name, "fun", node.id, null);
} else if (node.type === "FunctionExpression") {
// Function name goes in function's scope for named function expressions
node.$scope.add(node.id.name, "fun", node.id, null);
} else {
assert(false);
}
}
node.params.forEach(function(param) {
node.$scope.add(param.name, "param", param, null);
});
} else if (isForWithConstLet(node) || isForInOfWithConstLet(node)) {
// For(In/Of) loop with const|let declaration is a scope, with declaration in it
// There may be a block-scope under it
node.$scope = new Scope({
kind: "block",
node: node,
parent: parent.$scope,
});
} else if (node.type === "CatchClause") {
const identifier = node.param;
node.$scope = new Scope({
kind: "catch-block",
node: node,
parent: parent.$scope,
});
node.$scope.add(identifier.name, "caught", identifier, null);
// All hoist-scope keeps track of which variables that are propagated through,
// i.e. an reference inside the scope points to a declaration outside the scope.
// This is used to mark "taint" the name since adding a new variable in the scope,
// with a propagated name, would change the meaning of the existing references.
//
// catch(e) is special because even though e is a variable in its own scope,
// we want to make sure that catch(e){let e} is never transformed to
// catch(e){var e} (but rather var e$0). For that reason we taint the use of e
// in the closest hoist-scope, i.e. where var e$0 belongs.
node.$scope.closestHoistScope().markPropagates(identifier.name);
} else if (node.type === "Program") {
// Top-level program is a scope
// There's no block-scope under it
node.$scope = new Scope({
kind: "hoist",
node: node,
parent: null,
});
}
}
function createTopScope(programScope) {
function inject(obj) {
for (let name in obj) {
const writeable = obj[name];
const kind = (writeable ? "var" : "const");
if (topScope.hasOwn(name)) {
topScope.remove(name);
}
topScope.add(name, kind, {loc: {start: {line: -1}}}, -1);
}
}
const topScope = new Scope({
kind: "hoist",
node: {},
parent: null,
});
const complementary = {
undefined: false,
Infinity: false,
console: false,
};
inject(complementary);
// inject(jshint_vars.reservedVars);
// inject(jshint_vars.ecmaIdentifiers);
// link it in
programScope.parent = topScope;
topScope.children.push(programScope);
return topScope;
}
function isConstLet(kind) {
return kind === "const" || kind === "let";
}
function isNonFunctionBlock(node, parent) {
return node.type === "BlockStatement" && parent.type !== "FunctionDeclaration" && parent.type !== "FunctionExpression";
}
function isForWithConstLet(node) {
return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
}
function isForInOfWithConstLet(node) {
return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
}
function isForInOf(node) {
return node.type === "ForInStatement" || node.type === "ForOfStatement";
}
function isFunction(node) {
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
}
function isReference(node) {
const parent = node.$parent;
return node.$refToScope ||
node.type === "Identifier" &&
!(parent.type === "VariableDeclarator" && parent.id === node) && // var|let|const $
!(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) && // obj.$
!(parent.type === "Property" && parent.key === node) && // {$: ...}
!(parent.type === "LabeledStatement" && parent.label === node) && // $: ...
!(parent.type === "CatchClause" && parent.param === node) && // catch($)
!(isFunction(parent) && parent.id === node) && // function $(..
!(isFunction(parent) && is.someof(node, parent.params)) && // function f($)..
true;
}