UNPKG

10.4 kBJavaScriptView Raw
1var test = require('tape')
2var parse = require('acorn').parse
3var recast = require('recast')
4var ArrayFrom = require('array-from')
5var scan = require('../')
6
7function crawl (src, opts) {
8 var ast = parse(src, opts)
9 scan.crawl(ast)
10 return ast
11}
12
13function 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
22test('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
34test('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
45test('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]) // function declaration
52 t.ok(scope.has('a'), 'should find shadowed parameter')
53 t.ok(scope.has('b'), 'should find parameter')
54})
55
56test('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
70test('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
84test('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
106test('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 // try to rewrite some things
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
150test('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
169test('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
190test('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
208test('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
228test('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
252test('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
272test('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
296test('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
310test('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
331test('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
354test('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
370test('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})