1 | var test = require('tape')
|
2 | var parse = require('acorn').parse
|
3 | var ArrayFrom = require('array-from')
|
4 | var scan = require('../')
|
5 |
|
6 | function crawl (src, opts) {
|
7 | var ast = parse(src, opts)
|
8 | scan.crawl(ast)
|
9 | return ast
|
10 | }
|
11 |
|
12 | function cloneNode (node) {
|
13 | var cloned = {}
|
14 | var keys = Object.keys(node)
|
15 | for (var i = 0; i < keys.length; i++) {
|
16 | cloned[keys[i]] = node[keys[i]]
|
17 | }
|
18 | return cloned
|
19 | }
|
20 |
|
21 | test('register variable declarations in scope', function (t) {
|
22 | t.plan(5)
|
23 | var ast = crawl('var a, b; const c = 0; let d')
|
24 |
|
25 | var scope = scan.scope(ast)
|
26 | t.ok(scope.has('a'), 'should find var')
|
27 | t.ok(scope.has('b'), 'should find second declarator in var statement')
|
28 | t.ok(scope.has('c'), 'should find const')
|
29 | t.ok(scope.has('d'), 'should find let')
|
30 | t.notOk(scope.has('e'), 'nonexistent names should return false')
|
31 | })
|
32 |
|
33 | test('register variable declarations in block scope', function (t) {
|
34 | t.plan(4)
|
35 | var ast = crawl('var a, b; { let b; }')
|
36 | var scope = scan.scope(ast)
|
37 | t.ok(scope.has('a'))
|
38 | t.ok(scope.has('b'))
|
39 | scope = scan.scope(ast.body[1])
|
40 | t.ok(scope.has('b'), 'should declare `let` variable in BlockStatement scope')
|
41 | t.notOk(scope.has('a'), 'should only return true for names declared here')
|
42 | })
|
43 |
|
44 | test('register non variable declarations (function, class, parameter)', function (t) {
|
45 | t.plan(4)
|
46 | var ast = crawl('function a (b, a) {} class X {}')
|
47 | var scope = scan.scope(ast)
|
48 | t.ok(scope.has('a'), 'should find function declarations')
|
49 | t.ok(scope.has('X'), 'should find class definition')
|
50 | scope = scan.scope(ast.body[0])
|
51 | t.ok(scope.has('a'), 'should find shadowed parameter')
|
52 | t.ok(scope.has('b'), 'should find parameter')
|
53 | })
|
54 |
|
55 | test('use the value portion of a shorthand declaration property', function (t) {
|
56 | t.plan(2)
|
57 |
|
58 | var ast = parse('const { x } = y')
|
59 | var property = ast.body[0].declarations[0].id.properties[0]
|
60 | property.key = cloneNode(property.value)
|
61 | scan.crawl(ast)
|
62 |
|
63 | var binding = scan.scope(ast).getBinding('x')
|
64 |
|
65 | t.ok(binding.references.has(property.value))
|
66 | t.notOk(binding.references.has(property.key))
|
67 | })
|
68 |
|
69 | test('use the value portion of a shorthand object property', function (t) {
|
70 | t.plan(2)
|
71 |
|
72 | var ast = parse('({ x })')
|
73 | var property = ast.body[0].expression.properties[0]
|
74 | property.key = cloneNode(property.value)
|
75 | scan.crawl(ast)
|
76 |
|
77 | var binding = scan.scope(ast).undeclaredBindings.get('x')
|
78 |
|
79 | t.ok(binding.references.has(property.value))
|
80 | t.notOk(binding.references.has(property.key))
|
81 | })
|
82 |
|
83 | test('shadowing', function (t) {
|
84 | t.plan(8)
|
85 | var ast = crawl(`
|
86 | var a
|
87 | { let a }
|
88 | function b (b) {
|
89 | var a
|
90 | }
|
91 | `)
|
92 | var root = scan.scope(ast)
|
93 | var block = scan.scope(ast.body[1])
|
94 | var fn = scan.scope(ast.body[2])
|
95 | t.ok(root.has('a'), 'should find global var')
|
96 | t.ok(root.has('b'), 'should find function declaration')
|
97 | t.ok(block.has('a'), 'should shadow vars using `let` in block scope')
|
98 | t.notEqual(block.getBinding('a'), root.getBinding('a'), 'shadowing should define different bindings')
|
99 | t.ok(fn.has('b'), 'should find function parameter')
|
100 | t.notEqual(fn.getBinding('b'), root.getBinding('b'), 'shadowing function name with parameter should define different bindings')
|
101 | t.ok(fn.has('a'), 'should find local var')
|
102 | t.notEqual(fn.getBinding('a'), root.getBinding('a'), 'shadowing vars in function scope should define different bindings')
|
103 | })
|
104 |
|
105 | test('references', function (t) {
|
106 | t.plan(5)
|
107 |
|
108 | var src = `
|
109 | var a = 0
|
110 | a++
|
111 | a++
|
112 | function b (b) {
|
113 | console.log(b(a))
|
114 | }
|
115 | b(function (b) { return a + b })
|
116 | `
|
117 | var ast = crawl(src)
|
118 |
|
119 | var root = scan.scope(ast)
|
120 | var fn = scan.scope(ast.body[3])
|
121 | var callback = scan.scope(ast.body[4].expression.arguments[0])
|
122 |
|
123 | var a = root.getBinding('a')
|
124 | t.equal(a.getReferences().length, 5, 'should collect references in same and nested scopes')
|
125 | var b = root.getBinding('b')
|
126 | t.equal(b.getReferences().length, 2, 'should collect references to function declaration')
|
127 | var b2 = fn.getBinding('b')
|
128 | t.equal(b2.getReferences().length, 2, 'should collect references to shadowed function parameter')
|
129 | var b3 = callback.getBinding('b')
|
130 | t.equal(b3.getReferences().length, 2, 'should collect references to shadowed function parameter')
|
131 |
|
132 |
|
133 | var result = src.split('')
|
134 | a.getReferences().forEach(function (ref) { result[ref.start] = 'x' })
|
135 | b.getReferences().forEach(function (ref) { result[ref.start] = 'y' })
|
136 | b2.getReferences().forEach(function (ref) { result[ref.start] = 'z' })
|
137 | b3.getReferences().forEach(function (ref) { result[ref.start] = 'w' })
|
138 | t.equal(result.join(''), `
|
139 | var x = 0
|
140 | x++
|
141 | x++
|
142 | function y (z) {
|
143 | console.log(z(x))
|
144 | }
|
145 | y(function (w) { return x + w })
|
146 | `, 'references were associated correctly')
|
147 | })
|
148 |
|
149 | test('references that are declared later', function (t) {
|
150 | t.plan(4)
|
151 |
|
152 | var src = `
|
153 | if (true) { b(function () { c() }) }
|
154 | function b () {}
|
155 | function c () {}
|
156 | `
|
157 | var ast = crawl(src)
|
158 |
|
159 | var scope = scan.scope(ast)
|
160 | var b = scope.getBinding('b')
|
161 | t.ok(b, 'should have a binding for function b(){}')
|
162 | var c = scope.getBinding('c')
|
163 | t.ok(c, 'should have a binding for function c(){}')
|
164 | t.equal(b.getReferences().length, 2, 'should find all references for b')
|
165 | t.equal(c.getReferences().length, 2, 'should find all references for c')
|
166 | })
|
167 |
|
168 | test('shorthand properties', function (t) {
|
169 | t.plan(3)
|
170 |
|
171 | var src = `
|
172 | var b = 1
|
173 | var a = { b }
|
174 | var { c } = a
|
175 | console.log({ c, b, a })
|
176 | `
|
177 | var ast = crawl(src)
|
178 | var body = ast.body
|
179 |
|
180 | var scope = scan.scope(ast)
|
181 | var a = scope.getBinding('a')
|
182 | var b = scope.getBinding('b')
|
183 | var c = scope.getBinding('c')
|
184 | t.deepEqual(a.getReferences(), [a.definition, body[2].declarations[0].init, body[3].expression.arguments[0].properties[2].value])
|
185 | t.deepEqual(b.getReferences(), [b.definition, body[1].declarations[0].init.properties[0].value, body[3].expression.arguments[0].properties[1].value])
|
186 | t.deepEqual(c.getReferences(), [c.definition, body[3].expression.arguments[0].properties[0].value])
|
187 | })
|
188 |
|
189 | test('do not count object keys and method definitions as references', function (t) {
|
190 | t.plan(2)
|
191 |
|
192 | var src = `
|
193 | var a
|
194 | class B { a () {} }
|
195 | class C { get a () {} }
|
196 | class D { set a (b) {} }
|
197 | var e = { a: null }
|
198 | `
|
199 | var ast = crawl(src)
|
200 |
|
201 | var scope = scan.scope(ast)
|
202 | var a = scope.getBinding('a')
|
203 | t.equal(a.getReferences().length, 1)
|
204 | t.deepEqual(a.getReferences(), [a.definition])
|
205 | })
|
206 |
|
207 | test('do not count renamed imported identifiers as references', function (t) {
|
208 | t.plan(2)
|
209 |
|
210 | var src = `
|
211 | var a = 0
|
212 | a++
|
213 | a++
|
214 | import { a as b } from "b"
|
215 | b()
|
216 | `
|
217 | var ast = crawl(src, { sourceType: 'module' })
|
218 |
|
219 | var root = scan.scope(ast)
|
220 |
|
221 | var a = root.getBinding('a')
|
222 | var b = root.getBinding('b')
|
223 | t.equal(a.getReferences().length, 3, 'should not have counted renamed `a` import as a reference')
|
224 | t.equal(b.getReferences().length, 2, 'should have counted local name of renamed import')
|
225 | })
|
226 |
|
227 | test('remove references', function (t) {
|
228 | t.plan(6)
|
229 |
|
230 | var src = `
|
231 | function a () {}
|
232 | a()
|
233 | a()
|
234 | `
|
235 | var ast = crawl(src)
|
236 |
|
237 | var root = scan.scope(ast)
|
238 | var a = root.getBinding('a')
|
239 | t.equal(a.getReferences().length, 3, 'should have 3 references')
|
240 | t.ok(a.isReferenced(), 'should be referenced')
|
241 | var reference = ast.body[1].expression.callee
|
242 | a.remove(reference)
|
243 | t.equal(a.getReferences().length, 2, 'should have removed the reference')
|
244 | t.ok(a.isReferenced(), 'should still be referenced')
|
245 | reference = ast.body[2].expression.callee
|
246 | a.remove(reference)
|
247 | t.equal(a.getReferences().length, 1, 'should still have the definition reference')
|
248 | t.notOk(a.isReferenced(), 'should no longer be referenced')
|
249 | })
|
250 |
|
251 | test('collect references to undeclared variables', function (t) {
|
252 | t.plan(2)
|
253 |
|
254 | var src = `
|
255 | var a = b
|
256 | b = a
|
257 | a(b)
|
258 | function c () {
|
259 | return d
|
260 | }
|
261 | `
|
262 | var ast = crawl(src)
|
263 |
|
264 | var root = scan.scope(ast)
|
265 | var undeclared = ArrayFrom(root.undeclaredBindings.keys())
|
266 | var declared = ArrayFrom(root.bindings.keys())
|
267 | t.deepEqual(undeclared, ['b', 'd'])
|
268 | t.deepEqual(declared, ['a', 'c'])
|
269 | })
|
270 |
|
271 | test('loop over all available bindings, including declared in parent scope', function (t) {
|
272 | t.plan(1)
|
273 |
|
274 | var src = `
|
275 | var a = 0
|
276 | var b = 1, c = 2
|
277 | function d() {
|
278 | function e() {}
|
279 | function f() {
|
280 | var b = 3
|
281 | console.log('bindings')
|
282 | }
|
283 | }
|
284 | `
|
285 |
|
286 | var ast = crawl(src)
|
287 | var scope = scan.scope(ast.body[2].body.body[1])
|
288 | var names = []
|
289 | scope.forEachAvailable(function (binding, name) {
|
290 | names.push(name)
|
291 | })
|
292 | t.deepEqual(names, ['b', 'e', 'f', 'a', 'c', 'd'])
|
293 | })
|
294 |
|
295 | test('always initialise a scope for the root', function (t) {
|
296 | t.plan(2)
|
297 |
|
298 | var src = `
|
299 | console.log("null")
|
300 | `
|
301 |
|
302 | var ast = crawl(src)
|
303 | var scope = scan.scope(ast)
|
304 |
|
305 | t.ok(scope)
|
306 | t.deepEqual(scope.getUndeclaredNames(), ['console'])
|
307 | })
|
308 |
|
309 | test('initialises a scope for catch clauses', function (t) {
|
310 | t.plan(5)
|
311 | var ast = crawl(`
|
312 | var a = null
|
313 | a = 1
|
314 | try {
|
315 | } catch (a) {
|
316 | a = 2
|
317 | }
|
318 | `)
|
319 |
|
320 | var scope = scan.scope(ast)
|
321 | t.ok(scope.has('a'), 'should find var')
|
322 | t.equal(scope.getBinding('a').getReferences().length, 2, 'only counts references to outer `a`')
|
323 | var clause = ast.body[2].handler
|
324 | var catchScope = scan.scope(clause)
|
325 | t.ok(catchScope.has('a'), 'should find param')
|
326 | t.notEqual(scope.getBinding('a'), catchScope.getBinding('a'), 'introduced a different binding')
|
327 | t.equal(catchScope.getBinding('a').getReferences().length, 2, 'only counts references to inner `a`')
|
328 | })
|
329 |
|
330 | test('clear all scope information', function (t) {
|
331 | t.plan(6)
|
332 |
|
333 | var ast = crawl(`
|
334 | function x() {
|
335 | var y = z
|
336 | }
|
337 | var z = x
|
338 | `)
|
339 |
|
340 | var fn = ast.body[0]
|
341 |
|
342 | t.ok(scan.scope(ast))
|
343 | t.ok(scan.scope(fn))
|
344 | t.ok(scan.getBinding(fn.id))
|
345 |
|
346 | scan.clear(ast)
|
347 |
|
348 | t.notOk(scan.scope(ast))
|
349 | t.notOk(scan.scope(fn))
|
350 | t.notOk(scan.getBinding(fn.id))
|
351 | })
|
352 |
|
353 | test('clear partial scope information', function (t) {
|
354 | t.plan(4)
|
355 |
|
356 | var ast = crawl('function x() {}')
|
357 |
|
358 | var fn = ast.body[0]
|
359 |
|
360 | t.ok(scan.scope(fn))
|
361 | t.ok(scan.getBinding(fn.id))
|
362 |
|
363 | scan.deleteScope(fn)
|
364 |
|
365 | t.notOk(scan.scope(fn))
|
366 | t.ok(scan.getBinding(fn.id))
|
367 | })
|