pax_global_header00006660000000000000000000000064141270332720014513gustar00rootroot0000000000000052 comment=45cd4bfeff8ab85d664de5b3353d5c32be458cd0 scope-analyzer-2.1.2/000077500000000000000000000000001412703327200144515ustar00rootroot00000000000000scope-analyzer-2.1.2/.github/000077500000000000000000000000001412703327200160115ustar00rootroot00000000000000scope-analyzer-2.1.2/.github/dependabot.yml000066400000000000000000000002171412703327200206410ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 scope-analyzer-2.1.2/.github/workflows/000077500000000000000000000000001412703327200200465ustar00rootroot00000000000000scope-analyzer-2.1.2/.github/workflows/ci.yml000066400000000000000000000013631412703327200211670ustar00rootroot00000000000000name: Node CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: ['0.10', '0.12', 4.x, 6.x, 8.x, 10.x, 12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: npm install run: npm install - name: npm test run: npm run tests-only lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js 16.x uses: actions/setup-node@v2 with: node-version: 16.x - name: npm install run: npm install - name: npm run lint run: npm run lint scope-analyzer-2.1.2/.gitignore000066400000000000000000000010401412703327200164340ustar00rootroot00000000000000# Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules package-lock.json scope-analyzer-2.1.2/CHANGELOG.md000066400000000000000000000043211412703327200162620ustar00rootroot00000000000000# scope-analyzer change log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.1.2 / 2021-10-05 * make `.parent` and `[kScope]` properties non-enumerable, fixing compatibility with `recast`. anecdotally a 20-30% performance regression. you can pin to 2.1.1 if you need the 20% and don't need safe traversal of node properties. ## 2.1.1 / 2020-03-06 * use `dash-ast` for faster tree walking. anecdotally results in a 10-20% analysis speedup. ## 2.1.0 / 2020-03-06 * add `deleteScope()` and `clear()` methods to delete scope information from a single node or an entire tree. Thanks @fabiosantoscode! ## 2.0.6 / 2020-03-05 * detect the `.value` part of a shorthand property as the variable reference, instead of the `.key` part. This is a bugfix only for ASTs that were already modified prior to being crawled with scope-analyzer. Thanks @fabiosantoscode! ## 2.0.5 / 2018-06-25 * detect `catch(param){}` bindings ## 2.0.4 / 2018-05-22 * fix class method definition names being counted as references to outer variables. ## 2.0.3 / 2018-04-20 * revert to custom walker, acorn.walk behaviour is different and not faster ## 2.0.2 / 2018-04-20 * fix valid references that occur above a value is declared in source code ## 2.0.1 / 2018-03-30 * always initialise scope on the root node, so that undeclared names can be attached ## 2.0.0 / 2018-03-08 * add support for ES5 environments (Node 0.10+) * `scan.getBinding()` now also returns bindings for undeclared identifiers. `binding.definition` will be undefined for undeclared identifiers. ## 1.3.0 / 2018-01-13 * add `binding.remove(node)` to remove a reference to a binding from the list of references. use `binding.isReferenced()` to check if there are any references left. ## 1.2.0 / 2018-01-02 * track uses of undeclared variable names. use `getUndeclaredNames()` on the root scope to get a list of undeclared names used in the AST. ## 1.1.1 / 2018-01-02 * fix `import { a as b }` being counted as a reference to `a` ## 1.1.0 / 2017-12-26 * account for import declarations * rename `analyze` to `crawl` (analyze is still available as alias) * some tests ## 1.0.0 / 2017-11-15 * initial release scope-analyzer-2.1.2/LICENSE.md000066400000000000000000000011641412703327200160570ustar00rootroot00000000000000# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) Copyright 2017 Renée Kooi Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at > http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. scope-analyzer-2.1.2/README.md000066400000000000000000000110251412703327200157270ustar00rootroot00000000000000# scope-analyzer simple scope analysis for javascript ASTs. tracks scopes and collects references to variables. Caveats and/or todos: - May be missing edge cases. - Things like `label:`s are not considered at all, but ideally in the future they will! [![stability][stability-image]][stability-url] [![npm][npm-image]][npm-url] [![travis][travis-image]][travis-url] [![standard][standard-image]][standard-url] [stability-image]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square [stability-url]: https://nodejs.org/api/documentation.html#documentation_stability_index [npm-image]: https://img.shields.io/npm/v/scope-analyzer.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/scope-analyzer [travis-image]: https://img.shields.io/travis/goto-bus-stop/scope-analyzer.svg?style=flat-square [travis-url]: https://travis-ci.org/goto-bus-stop/scope-analyzer [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square [standard-url]: http://npm.im/standard ## Install ``` npm install scope-analyzer ``` ## Usage Note: AST nodes passed to `scope-analyzer` functions are expected to reference the parent node on a `node.parent` property. Nodes from [falafel](https://github.com/substack/node-falafel) or [transform-ast](https://github.com/goto-bus-stop/transform-ast) have a `.parent` property, but others may not. You can use [estree-assign-parent](https://github.com/goto-bus-stop/estree-assign-parent) to quickly assign a parent property to all nodes in an AST. ```js var scan = require('scope-analyzer') var ast = parse('...') // Initialize node module variables scan.createScope(ast, ['module', 'exports', '__dirname', '__filename']) scan.crawl(ast) var binding = scan.getBinding(ast, 'exports') binding.getReferences().forEach(function (reference) { // Assume for the sake of the example that all references to `exports` are assignments like // `exports.xyz = abc` console.log('found export:', reference.parent.property.name) }) ``` ## API ### `crawl(ast)` Walk the ast and analyze all scopes. This will immediately allow you to use the `get*` methods on any node in the tree. ### `clear(ast)` Clear scope information in all nodes of the AST. ### `visitScope(node)` Visit a node to check if it initialises any scopes. For example, a function declaration will initialise a new scope to hold bindings for its parameters. Use this if you are already walking the AST manually, and if you don't need the scope information during this walk. ### `visitBinding(node)` Visit a node to check if it is a reference to an existing binding. If it is, the reference is added to the parent scope. Use this if you are already walking the AST manually. ### `createScope(node, bindings)` Initialise a new scope at the given node. `bindings` is an array of variable names. This can be useful to make the scope analyzer aware of preexisting global variables. In that case, call `createScope` on the root node with the names of globals: ```js var ast = parse('xyz') scopeAnalyzer.createScope(ast, ['HTMLElement', 'Notification', ...]) ``` ### `deleteScope(node)` Delete the scope initialised by node. ### `scope(node)` Get the [Scope](#scope) initialised by the given node. ### `getBinding(node)` Get the [Binding](#binding) referenced by the `Identifier` `node`. ### Scope #### `scope.has(name)` Check if this scope defines `name`. #### `scope.getBinding(name)` Get the [Binding](#binding) named `name` that is declared by this scope. #### `scope.getReferences(name)` Get a list of all nodes referencing the `name` binding that is declared by this scope. #### `scope.getUndeclaredNames()` Get a list of all names that were used in this scope, but not defined anywhere in the AST. #### `scope.forEach(cb(binding, name))` Loop over all bindings declared by this scope. #### `scope.forEachAvailable(cb(binding, name))` Loop over all bindings available to this scope, declared in this scope or any parent scope. ### Binding #### `binding.definition` The node that defined this binding. If this binding was not declared in the AST, `binding.definition` will be undefined. #### `binding.getReferences()` Return an array of nodes that reference this binding. #### `binding.isReferenced()` Check if the binding is referenced, i.e., if there are any identifier Nodes (other than `binding.definition`) referencing this binding. #### `binding.remove(node)` Remove a reference to this binding. Use this when you are replacing the node referencing the binding with something else. ## License [Apache-2.0](LICENSE.md) scope-analyzer-2.1.2/bench/000077500000000000000000000000001412703327200155305ustar00rootroot00000000000000scope-analyzer-2.1.2/bench/d3.js000066400000000000000000000004301412703327200163710ustar00rootroot00000000000000var bench = require('nanobench') var parse = require('acorn').parse var scan = require('../') var src = require('fs').readFileSync(require.resolve('d3/build/d3'), 'utf8') bench('scope-analyze d3', function (b) { var ast = parse(src) b.start() scan.crawl(ast) b.end() }) scope-analyzer-2.1.2/bench/three.js000066400000000000000000000004301412703327200171720ustar00rootroot00000000000000var bench = require('nanobench') var parse = require('acorn').parse var scan = require('../') var src = require('fs').readFileSync(require.resolve('three'), 'utf8') bench('scope-analyze three.js', function (b) { var ast = parse(src) b.start() scan.crawl(ast) b.end() }) scope-analyzer-2.1.2/binding.js000066400000000000000000000016651412703327200164310ustar00rootroot00000000000000var Set = require('es6-set') module.exports = Binding function Binding (name, definition) { this.name = name this.definition = definition this.references = new Set() if (definition) this.add(definition) } Binding.prototype.add = function (node) { this.references.add(node) return this } Binding.prototype.remove = function (node) { if (!this.references.has(node)) { throw new Error('Tried removing nonexistent reference') } this.references.delete(node) return this } Binding.prototype.isReferenced = function () { var definition = this.definition var isReferenced = false this.each(function (ref) { if (ref !== definition) isReferenced = true }) return isReferenced } Binding.prototype.getReferences = function () { var arr = [] this.each(function (ref) { arr.push(ref) }) return arr } Binding.prototype.each = function (cb) { this.references.forEach(function (ref) { cb(ref) }) return this } scope-analyzer-2.1.2/index.js000066400000000000000000000161641412703327200161260ustar00rootroot00000000000000/* eslint-disable no-redeclare */ var assert = require('assert') var dash = require('dash-ast') var Symbol = require('es6-symbol') var getAssignedIdentifiers = require('get-assigned-identifiers') var isFunction = require('estree-is-function') var Binding = require('./binding') var Scope = require('./scope') var kScope = Symbol('scope') exports.createScope = createScope exports.visitScope = visitScope exports.visitBinding = visitBinding exports.crawl = crawl exports.analyze = crawl // old name exports.clear = clear exports.deleteScope = deleteScope exports.nearestScope = getNearestScope exports.scope = getScope exports.getBinding = getBinding function set (object, property, value) { Object.defineProperty(object, property, { value: value, enumerable: false, configurable: true, writable: true }) } // create a new scope at a node. function createScope (node, bindings) { assert.ok(typeof node === 'object' && node && typeof node.type === 'string', 'scope-analyzer: createScope: node must be an ast node') if (!node[kScope]) { var parent = getParentScope(node) set(node, kScope, new Scope(parent)) } if (bindings) { for (var i = 0; i < bindings.length; i++) { node[kScope].define(new Binding(bindings[i])) } } return node[kScope] } // Separate scope and binding registration steps, for post-order tree walkers. // Those will typically walk the scope-defining node _after_ the bindings that belong to that scope, // so they need to do it in two steps in order to define scopes first. function visitScope (node) { assert.ok(typeof node === 'object' && node && typeof node.type === 'string', 'scope-analyzer: visitScope: node must be an ast node') registerScopeBindings(node) } function visitBinding (node) { assert.ok(typeof node === 'object' && node && typeof node.type === 'string', 'scope-analyzer: visitBinding: node must be an ast node') if (isVariable(node)) { registerReference(node) } } function crawl (ast) { assert.ok(typeof ast === 'object' && ast && typeof ast.type === 'string', 'scope-analyzer: crawl: ast must be an ast node') dash(ast, function (node, parent) { set(node, 'parent', parent) visitScope(node) }) dash(ast, visitBinding) return ast } function clear (ast) { assert.ok(typeof ast === 'object' && ast && typeof ast.type === 'string', 'scope-analyzer: clear: ast must be an ast node') dash(ast, deleteScope) } function deleteScope (node) { if (node) { delete node[kScope] } } function getScope (node) { if (node && node[kScope]) { return node[kScope] } return null } function getBinding (identifier) { assert.strictEqual(typeof identifier, 'object', 'scope-analyzer: getBinding: identifier must be a node') assert.strictEqual(identifier.type, 'Identifier', 'scope-analyzer: getBinding: identifier must be an Identifier node') var scopeNode = getDeclaredScope(identifier) if (!scopeNode) return null var scope = getScope(scopeNode) if (!scope) return null return scope.getBinding(identifier.name) || scope.undeclaredBindings.get(identifier.name) } function registerScopeBindings (node) { if (node.type === 'Program') { createScope(node) } if (node.type === 'VariableDeclaration') { var scopeNode = getNearestScope(node, node.kind !== 'var') var scope = createScope(scopeNode) node.declarations.forEach(function (decl) { getAssignedIdentifiers(decl.id).forEach(function (id) { scope.define(new Binding(id.name, id)) }) }) } if (node.type === 'ClassDeclaration') { var scopeNode = getNearestScope(node) var scope = createScope(scopeNode) if (node.id && node.id.type === 'Identifier') { scope.define(new Binding(node.id.name, node.id)) } } if (node.type === 'FunctionDeclaration') { var scopeNode = getNearestScope(node, false) var scope = createScope(scopeNode) if (node.id && node.id.type === 'Identifier') { scope.define(new Binding(node.id.name, node.id)) } } if (isFunction(node)) { var scope = createScope(node) node.params.forEach(function (param) { getAssignedIdentifiers(param).forEach(function (id) { scope.define(new Binding(id.name, id)) }) }) } if (node.type === 'FunctionExpression' || node.type === 'ClassExpression') { var scope = createScope(node) if (node.id && node.id.type === 'Identifier') { scope.define(new Binding(node.id.name, node.id)) } } if (node.type === 'ImportDeclaration') { var scopeNode = getNearestScope(node, false) var scope = createScope(scopeNode) getAssignedIdentifiers(node).forEach(function (id) { scope.define(new Binding(id.name, id)) }) } if (node.type === 'CatchClause') { var scope = createScope(node) if (node.param) { getAssignedIdentifiers(node.param).forEach(function (id) { scope.define(new Binding(id.name, id)) }) } } } function getParentScope (node) { var parent = node while (parent.parent) { parent = parent.parent if (getScope(parent)) return getScope(parent) } } // Get the scope that a declaration will be declared in function getNearestScope (node, blockScope) { var parent = node while (parent.parent) { parent = parent.parent if (isFunction(parent)) { break } if (blockScope && parent.type === 'BlockStatement') { break } if (parent.type === 'Program') { break } } return parent } // Get the scope that this identifier has been declared in function getDeclaredScope (id) { var parent = id // Jump over one parent if this is a function's name--the variables // and parameters _inside_ the function are attached to the FunctionDeclaration // so if a variable inside the function has the same name as the function, // they will conflict. // Here we jump out of the FunctionDeclaration so we can start by looking at the // surrounding scope if (id.parent.type === 'FunctionDeclaration' && id.parent.id === id) { parent = id.parent } while (parent.parent) { parent = parent.parent if (parent[kScope] && parent[kScope].has(id.name)) { break } } return parent } function registerReference (node) { var scopeNode = getDeclaredScope(node) var scope = getScope(scopeNode) if (scope && scope.has(node.name)) { scope.add(node.name, node) } if (scope && !scope.has(node.name)) { scope.addUndeclared(node.name, node) } } function isObjectKey (node) { return node.parent.type === 'Property' && node.parent.key === node && // a shorthand property may have the ===-same node as both the key and the value. // we should detect the value part. node.parent.value !== node } function isMethodDefinition (node) { return node.parent.type === 'MethodDefinition' && node.parent.key === node } function isImportName (node) { return node.parent.type === 'ImportSpecifier' && node.parent.imported === node } function isVariable (node) { return node.type === 'Identifier' && !isObjectKey(node) && !isMethodDefinition(node) && (node.parent.type !== 'MemberExpression' || node.parent.object === node || (node.parent.property === node && node.parent.computed)) && !isImportName(node) } scope-analyzer-2.1.2/package.json000066400000000000000000000024721412703327200167440ustar00rootroot00000000000000{ "name": "scope-analyzer", "description": "simple scope analysis for javascript ASTs", "version": "2.1.2", "author": "Renée Kooi ", "bugs": { "url": "https://github.com/goto-bus-stop/scope-analyzer/issues" }, "dependencies": { "array-from": "^2.1.1", "dash-ast": "^2.0.1", "es6-map": "^0.1.5", "es6-set": "^0.1.5", "es6-symbol": "^3.1.1", "estree-is-function": "^1.0.0", "get-assigned-identifiers": "^1.1.0" }, "devDependencies": { "acorn": "^8.0.1", "babel-core": "^6.26.3", "babel-plugin-transform-es2015-template-literals": "^6.22.0", "d3": "^4.13.0", "has-template-literals": "^1.0.0", "nanobench": "^2.1.1", "recast": "^0.20.5", "standard": "^14.3.1", "tape": "^5.0.1", "three": "^0.89.0" }, "homepage": "https://github.com/goto-bus-stop/scope-analyzer", "keywords": [ "analysis", "ast", "javascript", "nodes", "refactor", "rename", "scope" ], "license": "Apache-2.0", "main": "index.js", "repository": { "type": "git", "url": "https://github.com/goto-bus-stop/scope-analyzer.git" }, "scripts": { "bench": "nanobench bench/*.js", "lint": "standard", "tests-only": "node -r ./test/_init.js test/index.js", "test": "npm run lint && npm run tests-only" } } scope-analyzer-2.1.2/scope.js000066400000000000000000000033641412703327200161260ustar00rootroot00000000000000var Map = require('es6-map') var Set = require('es6-set') var ArrayFrom = require('array-from') var Binding = require('./binding') module.exports = Scope function Scope (parent) { this.parent = parent this.bindings = new Map() this.undeclaredBindings = new Map() } Scope.prototype.define = function (binding) { if (this.bindings.has(binding.name)) { var existing = this.bindings.get(binding.name) binding.getReferences().forEach(function (ref) { existing.add(ref) }) } else { this.bindings.set(binding.name, binding) } return this } Scope.prototype.has = function (name) { return this.bindings.has(name) } Scope.prototype.add = function (name, ref) { var binding = this.bindings.get(name) if (binding) { binding.add(ref) } return this } Scope.prototype.addUndeclared = function (name, ref) { if (!this.undeclaredBindings.has(name)) { this.undeclaredBindings.set(name, new Binding(name)) } var binding = this.undeclaredBindings.get(name) binding.add(ref) return this } Scope.prototype.getBinding = function (name) { return this.bindings.get(name) } Scope.prototype.getReferences = function (name) { return this.has(name) ? this.bindings.get(name).getReferences() : [] } Scope.prototype.getUndeclaredNames = function () { return ArrayFrom(this.undeclaredBindings.keys()) } Scope.prototype.forEach = function () { this.bindings.forEach.apply(this.bindings, arguments) } Scope.prototype.forEachAvailable = function (cb) { var seen = new Set() this.bindings.forEach(function (binding, name) { seen.add(name) cb(binding, name) }) this.parent && this.parent.forEachAvailable(function (binding, name) { if (!seen.has(name)) { seen.add(name) cb(binding, name) } }) } scope-analyzer-2.1.2/test/000077500000000000000000000000001412703327200154305ustar00rootroot00000000000000scope-analyzer-2.1.2/test/_init.js000066400000000000000000000002111412703327200170620ustar00rootroot00000000000000if (!require('has-template-literals')()) { require('babel-core/register')({ plugins: ['transform-es2015-template-literals'] }) } scope-analyzer-2.1.2/test/index.js000066400000000000000000000242611412703327200171020ustar00rootroot00000000000000var test = require('tape') var parse = require('acorn').parse var recast = require('recast') var ArrayFrom = require('array-from') var scan = require('../') function crawl (src, opts) { var ast = parse(src, opts) scan.crawl(ast) return ast } function cloneNode (node) { var cloned = {} var keys = Object.keys(node) for (var i = 0; i < keys.length; i++) { cloned[keys[i]] = node[keys[i]] } return cloned } test('register variable declarations in scope', function (t) { t.plan(5) var ast = crawl('var a, b; const c = 0; let d') var scope = scan.scope(ast) t.ok(scope.has('a'), 'should find var') t.ok(scope.has('b'), 'should find second declarator in var statement') t.ok(scope.has('c'), 'should find const') t.ok(scope.has('d'), 'should find let') t.notOk(scope.has('e'), 'nonexistent names should return false') }) test('register variable declarations in block scope', function (t) { t.plan(4) var ast = crawl('var a, b; { let b; }') var scope = scan.scope(ast) t.ok(scope.has('a')) t.ok(scope.has('b')) scope = scan.scope(ast.body[1]) t.ok(scope.has('b'), 'should declare `let` variable in BlockStatement scope') t.notOk(scope.has('a'), 'should only return true for names declared here') }) test('register non variable declarations (function, class, parameter)', function (t) { t.plan(4) var ast = crawl('function a (b, a) {} class X {}') var scope = scan.scope(ast) t.ok(scope.has('a'), 'should find function declarations') t.ok(scope.has('X'), 'should find class definition') scope = scan.scope(ast.body[0]) // function declaration t.ok(scope.has('a'), 'should find shadowed parameter') t.ok(scope.has('b'), 'should find parameter') }) test('use the value portion of a shorthand declaration property', function (t) { t.plan(2) var ast = parse('const { x } = y') var property = ast.body[0].declarations[0].id.properties[0] property.key = cloneNode(property.value) scan.crawl(ast) var binding = scan.scope(ast).getBinding('x') t.ok(binding.references.has(property.value)) t.notOk(binding.references.has(property.key)) }) test('use the value portion of a shorthand object property', function (t) { t.plan(2) var ast = parse('({ x })') var property = ast.body[0].expression.properties[0] property.key = cloneNode(property.value) scan.crawl(ast) var binding = scan.scope(ast).undeclaredBindings.get('x') t.ok(binding.references.has(property.value)) t.notOk(binding.references.has(property.key)) }) test('shadowing', function (t) { t.plan(8) var ast = crawl(` var a { let a } function b (b) { var a } `) var root = scan.scope(ast) var block = scan.scope(ast.body[1]) var fn = scan.scope(ast.body[2]) t.ok(root.has('a'), 'should find global var') t.ok(root.has('b'), 'should find function declaration') t.ok(block.has('a'), 'should shadow vars using `let` in block scope') t.notEqual(block.getBinding('a'), root.getBinding('a'), 'shadowing should define different bindings') t.ok(fn.has('b'), 'should find function parameter') t.notEqual(fn.getBinding('b'), root.getBinding('b'), 'shadowing function name with parameter should define different bindings') t.ok(fn.has('a'), 'should find local var') t.notEqual(fn.getBinding('a'), root.getBinding('a'), 'shadowing vars in function scope should define different bindings') }) test('references', function (t) { t.plan(5) var src = ` var a = 0 a++ a++ function b (b) { console.log(b(a)) } b(function (b) { return a + b }) ` var ast = crawl(src) var root = scan.scope(ast) var fn = scan.scope(ast.body[3]) var callback = scan.scope(ast.body[4].expression.arguments[0]) var a = root.getBinding('a') t.equal(a.getReferences().length, 5, 'should collect references in same and nested scopes') var b = root.getBinding('b') t.equal(b.getReferences().length, 2, 'should collect references to function declaration') var b2 = fn.getBinding('b') t.equal(b2.getReferences().length, 2, 'should collect references to shadowed function parameter') var b3 = callback.getBinding('b') t.equal(b3.getReferences().length, 2, 'should collect references to shadowed function parameter') // try to rewrite some things var result = src.split('') a.getReferences().forEach(function (ref) { result[ref.start] = 'x' }) b.getReferences().forEach(function (ref) { result[ref.start] = 'y' }) b2.getReferences().forEach(function (ref) { result[ref.start] = 'z' }) b3.getReferences().forEach(function (ref) { result[ref.start] = 'w' }) t.equal(result.join(''), ` var x = 0 x++ x++ function y (z) { console.log(z(x)) } y(function (w) { return x + w }) `, 'references were associated correctly') }) test('references that are declared later', function (t) { t.plan(4) var src = ` if (true) { b(function () { c() }) } function b () {} function c () {} ` var ast = crawl(src) var scope = scan.scope(ast) var b = scope.getBinding('b') t.ok(b, 'should have a binding for function b(){}') var c = scope.getBinding('c') t.ok(c, 'should have a binding for function c(){}') t.equal(b.getReferences().length, 2, 'should find all references for b') t.equal(c.getReferences().length, 2, 'should find all references for c') }) test('shorthand properties', function (t) { t.plan(3) var src = ` var b = 1 var a = { b } var { c } = a console.log({ c, b, a }) ` var ast = crawl(src) var body = ast.body var scope = scan.scope(ast) var a = scope.getBinding('a') var b = scope.getBinding('b') var c = scope.getBinding('c') t.deepEqual(a.getReferences(), [a.definition, body[2].declarations[0].init, body[3].expression.arguments[0].properties[2].value]) t.deepEqual(b.getReferences(), [b.definition, body[1].declarations[0].init.properties[0].value, body[3].expression.arguments[0].properties[1].value]) t.deepEqual(c.getReferences(), [c.definition, body[3].expression.arguments[0].properties[0].value]) }) test('do not count object keys and method definitions as references', function (t) { t.plan(2) var src = ` var a class B { a () {} } class C { get a () {} } class D { set a (b) {} } var e = { a: null } ` var ast = crawl(src) var scope = scan.scope(ast) var a = scope.getBinding('a') t.equal(a.getReferences().length, 1) t.deepEqual(a.getReferences(), [a.definition]) }) test('do not count renamed imported identifiers as references', function (t) { t.plan(2) var src = ` var a = 0 a++ a++ import { a as b } from "b" b() ` var ast = crawl(src, { sourceType: 'module' }) var root = scan.scope(ast) var a = root.getBinding('a') var b = root.getBinding('b') t.equal(a.getReferences().length, 3, 'should not have counted renamed `a` import as a reference') t.equal(b.getReferences().length, 2, 'should have counted local name of renamed import') }) test('remove references', function (t) { t.plan(6) var src = ` function a () {} a() a() ` var ast = crawl(src) var root = scan.scope(ast) var a = root.getBinding('a') t.equal(a.getReferences().length, 3, 'should have 3 references') t.ok(a.isReferenced(), 'should be referenced') var reference = ast.body[1].expression.callee a.remove(reference) t.equal(a.getReferences().length, 2, 'should have removed the reference') t.ok(a.isReferenced(), 'should still be referenced') reference = ast.body[2].expression.callee a.remove(reference) t.equal(a.getReferences().length, 1, 'should still have the definition reference') t.notOk(a.isReferenced(), 'should no longer be referenced') }) test('collect references to undeclared variables', function (t) { t.plan(2) var src = ` var a = b b = a a(b) function c () { return d } ` var ast = crawl(src) var root = scan.scope(ast) var undeclared = ArrayFrom(root.undeclaredBindings.keys()) var declared = ArrayFrom(root.bindings.keys()) t.deepEqual(undeclared, ['b', 'd']) t.deepEqual(declared, ['a', 'c']) }) test('loop over all available bindings, including declared in parent scope', function (t) { t.plan(1) var src = ` var a = 0 var b = 1, c = 2 function d() { function e() {} function f() { var b = 3 console.log('bindings') } } ` var ast = crawl(src) var scope = scan.scope(ast.body[2].body.body[1]) var names = [] scope.forEachAvailable(function (binding, name) { names.push(name) }) t.deepEqual(names, ['b', 'e', 'f', 'a', 'c', 'd']) }) test('always initialise a scope for the root', function (t) { t.plan(2) var src = ` console.log("null") ` var ast = crawl(src) var scope = scan.scope(ast) t.ok(scope) t.deepEqual(scope.getUndeclaredNames(), ['console']) }) test('initialises a scope for catch clauses', function (t) { t.plan(5) var ast = crawl(` var a = null a = 1 try { } catch (a) { a = 2 } `) var scope = scan.scope(ast) t.ok(scope.has('a'), 'should find var') t.equal(scope.getBinding('a').getReferences().length, 2, 'only counts references to outer `a`') var clause = ast.body[2].handler var catchScope = scan.scope(clause) t.ok(catchScope.has('a'), 'should find param') t.notEqual(scope.getBinding('a'), catchScope.getBinding('a'), 'introduced a different binding') t.equal(catchScope.getBinding('a').getReferences().length, 2, 'only counts references to inner `a`') }) test('clear all scope information', function (t) { t.plan(6) var ast = crawl(` function x() { var y = z } var z = x `) var fn = ast.body[0] t.ok(scan.scope(ast)) t.ok(scan.scope(fn)) t.ok(scan.getBinding(fn.id)) scan.clear(ast) t.notOk(scan.scope(ast)) t.notOk(scan.scope(fn)) t.notOk(scan.getBinding(fn.id)) }) test('clear partial scope information', function (t) { t.plan(4) var ast = crawl('function x() {}') var fn = ast.body[0] t.ok(scan.scope(fn)) t.ok(scan.getBinding(fn.id)) scan.deleteScope(fn) t.notOk(scan.scope(fn)) t.ok(scan.getBinding(fn.id)) }) test('recast: does not touch all nodes', function (t) { t.plan(1) var input = 'function *weirdly(){ const formatted =0; }' var ast = recast.parse(input) scan.analyze(ast) var output = recast.print(ast).code t.equal(input, output) })