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