tree-hugger-js
Version:
A friendly tree-sitter wrapper for JavaScript and TypeScript
282 lines • 9.79 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeNode = void 0;
const pattern_parser_1 = require("./pattern-parser");
const visitor_1 = require("./visitor");
const errors_1 = require("./errors");
class TreeNode {
constructor(node, sourceCode, parent) {
this.node = node;
this.sourceCode = sourceCode;
this.parent = parent;
// Defensive check for undefined node - addresses tree-sitter race condition in CI environments
if (!node) {
throw new errors_1.ParseError('TreeNode constructor received undefined SyntaxNode. This may be caused by a race condition in tree-sitter native bindings, commonly seen in CI environments with concurrent test execution.');
}
// Validate that the node has required properties
if (typeof node.type === 'undefined') {
throw new errors_1.ParseError('TreeNode constructor received invalid SyntaxNode - missing type property. This indicates a problem with tree-sitter native binding initialization.');
}
}
get text() {
return this.sourceCode.slice(this.node.startIndex, this.node.endIndex);
}
get type() {
return this.node.type;
}
get startPosition() {
return this.node.startPosition;
}
get endPosition() {
return this.node.endPosition;
}
get children() {
this._children ?? (this._children = this.node.children.map(child => new TreeNode(child, this.sourceCode, this)));
return this._children;
}
get line() {
return this.startPosition.row + 1;
}
get column() {
return this.startPosition.column + 1;
}
get endLine() {
return this.endPosition.row + 1;
}
get hasError() {
return this.node.hasError;
}
get name() {
const nameNode = this.node.childForFieldName('name');
return nameNode ? this.sourceCode.slice(nameNode.startIndex, nameNode.endIndex) : undefined;
}
/**
* Extract parameters from function-like nodes (functions, methods, arrow functions)
*/
extractParameters() {
const parameters = [];
// Handle different function types
if (this.type === 'function_declaration' ||
this.type === 'function_expression' ||
this.type === 'method_definition' ||
this.type === 'arrow_function') {
const paramsNode = this.node.childForFieldName('parameters');
if (paramsNode) {
const paramsWrapper = new TreeNode(paramsNode, this.sourceCode, this);
// Extract formal parameters - look directly in the formal_parameters node
for (const child of paramsWrapper.children) {
if (child.type === 'identifier' ||
child.type === 'rest_pattern' ||
child.type === 'assignment_pattern' ||
child.type === 'object_pattern' ||
child.type === 'array_pattern' ||
child.type === 'required_parameter') {
parameters.push(child.text);
}
}
}
else {
// For some function types, look for formal_parameters as direct child
for (const child of this.children) {
if (child.type === 'formal_parameters') {
for (const param of child.children) {
if (param.type === 'identifier' ||
param.type === 'rest_pattern' ||
param.type === 'assignment_pattern' ||
param.type === 'object_pattern' ||
param.type === 'array_pattern' ||
param.type === 'required_parameter') {
parameters.push(param.text);
}
}
}
}
}
}
return parameters;
}
/**
* Check if this function-like node is async
*/
isAsync() {
if (this.type === 'function_declaration' ||
this.type === 'function_expression' ||
this.type === 'method_definition' ||
this.type === 'arrow_function') {
// Check for async modifier
for (const child of this.children) {
if (child.type === 'async' || child.text === 'async') {
return true;
}
}
// Also check the text content as fallback
return this.text.includes('async');
}
return false;
}
/**
* Get the body range of a function or class
*/
getBodyRange() {
const bodyNode = this.node.childForFieldName('body');
if (bodyNode) {
return {
startLine: bodyNode.startPosition.row + 1,
endLine: bodyNode.endPosition.row + 1,
};
}
return null;
}
// Navigation methods
find(pattern) {
const predicate = this.parsePattern(pattern);
return this.findNode(predicate);
}
findAll(pattern) {
const predicate = this.parsePattern(pattern);
return this.findAllNodes(predicate);
}
findNode(predicate) {
if (predicate(this))
return this;
for (const child of this.children) {
const result = child.findNode(predicate);
if (result)
return result;
}
return null;
}
findAllNodes(predicate) {
const results = [];
if (predicate(this)) {
results.push(this);
}
for (const child of this.children) {
results.push(...child.findAllNodes(predicate));
}
return results;
}
parsePattern(pattern) {
try {
return new pattern_parser_1.PatternParser().parse(pattern);
}
catch {
// Fallback to simple type matching for backward compatibility
return (node) => node.type === pattern;
}
}
// Common queries
functions() {
return this.findAll('function');
}
classes() {
return this.findAll('class');
}
imports() {
return this.findAll('import_statement');
}
variables() {
return this.findAll('variable_declarator');
}
comments() {
return this.findAll('comment');
}
// Export analysis
exports() {
return this.findAll('export_statement').concat(this.findAll('export_specifier'));
}
// JSX-specific helpers
jsxComponents() {
return this.findAll('jsx_element').concat(this.findAll('jsx_self_closing_element'));
}
jsxProps(componentName) {
const components = componentName
? this.jsxComponents().filter(c => {
const opening = c.find('jsx_opening_element');
const name = opening?.find('identifier')?.text ?? opening?.find('member_expression')?.text;
return name === componentName;
})
: this.jsxComponents();
const props = [];
components.forEach(component => {
props.push(...component.findAll('jsx_attribute'));
});
return props;
}
// React hooks
hooks() {
return this.findAll('call_expression').filter(call => {
const func = call.node.childForFieldName('function');
return func && func.text.startsWith('use') && /^use[A-Z]/.test(func.text);
});
}
// Parent/sibling navigation
getParent(type) {
let current = this.parent;
while (current) {
if (!type || current.type === type)
return current;
current = current.parent;
}
return null;
}
siblings() {
if (!this.parent)
return [];
return this.parent.children.filter(child => child !== this);
}
ancestors() {
const result = [];
let current = this.parent;
while (current) {
result.push(current);
current = current.parent;
}
return result;
}
descendants(type) {
return type ? this.findAll(type) : this.getAllDescendants();
}
getAllDescendants() {
const result = [];
for (const child of this.children) {
result.push(child);
result.push(...child.getAllDescendants());
}
return result;
}
// Visitor pattern support
visit(visitor) {
(0, visitor_1.visit)(this, visitor);
}
// Get path from this node to root
getPath() {
const path = [this];
let current = this.parent;
while (current) {
path.unshift(current);
current = current.parent;
}
return path;
}
// Find node at specific position
nodeAt(line, column) {
const pos = { row: line - 1, column: column - 1 };
if (this.startPosition.row > pos.row ||
(this.startPosition.row === pos.row && this.startPosition.column > pos.column) ||
this.endPosition.row < pos.row ||
(this.endPosition.row === pos.row && this.endPosition.column < pos.column)) {
return null;
}
// Check children first (more specific)
for (const child of this.children) {
const found = child.nodeAt(line, column);
if (found)
return found;
}
// This node contains the position
return this;
}
}
exports.TreeNode = TreeNode;
//# sourceMappingURL=node-wrapper.js.map