Scope Analyzer
scope-analyzer is a JavaScript library designed for performing basic scope analysis on JavaScript Abstract Syntax Trees (ASTs). It tracks variable scopes and collects references to variables within a given AST, enabling tasks like refactoring, renaming, and understanding variable usage patterns. The current stable version is 2.1.2. The package has seen consistent minor and patch releases, indicating active maintenance, though its stability badge still labels it as "experimental." A key differentiator is its focus on simplicity and direct manipulation of AST nodes, providing methods to crawl the tree, create, delete, and inspect scopes, and retrieve bindings and references. It is particularly useful for tools that need to understand the lexical environment of JavaScript code. It expects AST nodes to have a `.parent` property, often requiring a pre-processing step with utilities like `estree-assign-parent`.
Common errors
-
TypeError: Cannot read property 'parent' of undefined
cause The AST nodes passed to `scope-analyzer` do not have a `.parent` property, which is required for its internal traversal logic.fixPreprocess your AST using a utility like `estree-assign-parent` to add the `.parent` property to all nodes. Example: `require('estree-assign-parent')(ast);` -
ReferenceError: require is not defined
cause You are attempting to use the CommonJS `require` function in an ES Module environment (e.g., a `.mjs` file or a project with `"type": "module"` in `package.json`).fixIf in an ESM context, consider using dynamic `import()` (`const scan = await import('scope-analyzer');`) or configure your bundler/Node.js environment to properly handle CommonJS modules. -
TypeError: scan.crawl is not a function
cause The `scope-analyzer` module was imported incorrectly, preventing the `scan` variable from holding the expected object with its API methods. This often happens with incorrect destructuring or ES Module imports.fixEnsure you are using the correct CommonJS import: `const scan = require('scope-analyzer');`. Avoid named imports (`import { crawl } from 'scope-analyzer';`) as the package exports a single object.
Warnings
- gotcha The library critically relies on AST nodes having a `.parent` property for correct traversal and scope resolution. Most standard parsers (e.g., Acorn, Esprima) do not automatically provide this, requiring manual preprocessing of the AST.
- breaking Version 2.1.2 introduced a change making `.parent` and `[kScope]` properties non-enumerable. While this resolves compatibility issues with `recast`, it can lead to a significant performance regression of 20-30% in analysis speed compared to v2.1.1.
- gotcha The package's stability badge labels it as "experimental." This implies that while actively maintained, its API or internal behavior might still undergo changes in future versions, potentially without strict adherence to semantic versioning for minor changes.
- breaking As of version 2.0.0, `scan.getBinding()` will now return a `Binding` object even for undeclared identifiers. In such cases, the `binding.definition` property will be `undefined`.
- gotcha This library is published as a CommonJS module and does not officially support or document direct ES Module (`import`) syntax. Attempting to import it directly in an ESM context might result in errors or unexpected behavior depending on your bundler or Node.js version.
Install
-
npm install scope-analyzer -
yarn add scope-analyzer -
pnpm add scope-analyzer
Imports
- scan
import scan from 'scope-analyzer'
const scan = require('scope-analyzer') - crawl
crawl(ast)
scan.crawl(ast)
- createScope
createScope(node, bindings)
scan.createScope(node, bindings)
- getBinding
getBinding(node, name)
scan.getBinding(node, name)
Quickstart
const parse = require('acorn').parse;
const scan = require('scope-analyzer');
// Example JavaScript source code
const code = `
function greet(name) {
const message = 'Hello, ' + name;
console.log(message);
exports.greeting = message; // Example of an 'exports' reference
}
greet('World');
var globalVar = 10;
`;
// Parse the code into an AST. 'acorn' is a common choice.
// Ensure to specify ecmaVersion and sourceType for accurate parsing.
const ast = parse(code, { ecmaVersion: 2020, sourceType: 'script' });
// IMPORTANT: scope-analyzer expects AST nodes to have a `node.parent` property.
// Most parsers (like Acorn) do not add this by default.
// Uncomment the following line and install 'estree-assign-parent' if your AST lacks parent pointers:
// require('estree-assign-parent')(ast);
// Initialize the root scope with common Node.js global variables
// This makes 'exports' a known binding for analysis.
scan.createScope(ast, ['module', 'exports', '__dirname', '__filename', 'console']);
// Crawl the AST to analyze all scopes and collect variable references.
scan.crawl(ast);
// Example 1: Find all references to the 'exports' binding
const exportsBinding = scan.getBinding(ast, 'exports');
if (exportsBinding) {
console.log('--- References to "exports" ---');
exportsBinding.getReferences().forEach(function (reference) {
// Assuming references like `exports.property = value`
if (reference.parent && reference.parent.type === 'MemberExpression' && reference.parent.property) {
console.log(`- Exported property: '${reference.parent.property.name}' at line ${reference.loc.start.line}`);
} else {
console.log(`- General reference at line ${reference.loc.start.line}`);
}
});
} else {
console.log('No binding found for "exports".');
}
// Example 2: Find undeclared names in the root scope
const rootScope = scan.scope(ast);
if (rootScope) {
const undeclared = rootScope.getUndeclaredNames();
if (undeclared.length > 0) {
console.log('\n--- Undeclared names in root scope ---');
console.log(`- ${undeclared.join(', ')}`);
} else {
console.log('\nNo undeclared names found in root scope.');
}
}