tree-hugger-js
Version:
A friendly tree-sitter wrapper for JavaScript and TypeScript
172 lines • 5.42 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScopeAnalyzer = exports.TreeVisitor = void 0;
exports.visit = visit;
class TreeVisitor {
visit(node, visitor) {
// Handle simple function visitor
if (typeof visitor === 'function') {
visitor = { enter: visitor };
}
this.visitNode(node, visitor, undefined);
}
visitNode(node, visitor, parent) {
// Call enter visitor
if (visitor.enter) {
const result = visitor.enter(node, parent);
// If visitor returns false, stop entire traversal
if (result === false) {
return false;
}
}
// Visit children
for (const child of node.children) {
const shouldContinue = this.visitNode(child, visitor, node);
if (!shouldContinue) {
return false;
}
}
// Call exit visitor
if (visitor.exit) {
const result = visitor.exit(node, parent);
if (result === false) {
return false;
}
}
return true;
}
// Utility method to collect nodes matching a condition
collect(node, predicate) {
const results = [];
this.visit(node, n => {
if (predicate(n)) {
results.push(n);
}
});
return results;
}
// Find the first node matching a condition
findFirst(node, predicate) {
let result = null;
this.visit(node, n => {
if (predicate(n)) {
result = n;
return false; // Stop traversal
}
});
return result;
}
// Get the path from root to a specific node
getPath(root, target) {
const path = [];
let found = false;
const traverse = (node, currentPath) => {
currentPath.push(node);
if (node === target) {
path.push(...currentPath);
found = true;
return false;
}
for (const child of node.children) {
if (!traverse(child, currentPath)) {
return false;
}
}
currentPath.pop();
return true;
};
traverse(root, []);
return found ? path : [];
}
}
exports.TreeVisitor = TreeVisitor;
// Convenience function for visiting
function visit(node, visitor) {
new TreeVisitor().visit(node, visitor);
}
class ScopeAnalyzer {
constructor() {
this.scopes = new Map();
this.currentScope = null;
}
analyze(root) {
this.scopes.clear();
// Create root scope
const rootScope = {
node: root,
bindings: new Map(),
parent: undefined,
};
this.scopes.set(root, rootScope);
this.currentScope = rootScope;
visit(root, {
enter: node => {
if (this.createsScope(node)) {
const scope = {
node,
bindings: new Map(),
parent: this.currentScope ?? undefined,
};
this.scopes.set(node, scope);
this.currentScope = scope;
}
// Track variable declarations
if (node.type === 'variable_declarator') {
const name = node.name;
if (name && this.currentScope) {
this.currentScope.bindings.set(name, node);
}
}
// Track function parameters
if (node.type === 'formal_parameters' && this.currentScope) {
// Look for parameter nodes which might be identifiers or patterns
const params = node.findAll('identifier');
params.forEach(param => {
if (param.text) {
this.currentScope?.bindings.set(param.text, param);
}
});
}
},
exit: node => {
if (this.createsScope(node) && this.currentScope) {
this.currentScope = this.currentScope.parent ?? null;
}
},
});
return this.scopes;
}
createsScope(node) {
return [
'function_declaration',
'function_expression',
'arrow_function',
'method_definition',
'class_declaration',
'class_expression',
'for_statement',
'for_in_statement',
'for_of_statement',
'catch_clause',
].includes(node.type);
}
getScope(node) {
return this.scopes.get(node);
}
findBinding(node, name) {
let current = node;
while (current) {
const scope = this.scopes.get(current);
if (scope?.bindings.has(name)) {
return scope.bindings.get(name) ?? null;
}
// Move up to parent scope
if (!current.parent)
break;
current = current.parent;
}
return null;
}
}
exports.ScopeAnalyzer = ScopeAnalyzer;
//# sourceMappingURL=visitor.js.map