tree-hugger-js
Version:
A friendly tree-sitter wrapper for JavaScript and TypeScript
227 lines • 8.58 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeNode = exports.TreeHugger = void 0;
exports.parse = parse;
const tree_sitter_1 = __importDefault(require("tree-sitter"));
const fs_1 = require("fs");
const languages_1 = require("./languages");
const node_wrapper_1 = require("./node-wrapper");
const transform_1 = require("./transform");
const errors_1 = require("./errors");
const visitor_1 = require("./visitor");
class TreeHugger {
constructor(sourceCode, options = {}) {
this.parser = new tree_sitter_1.default();
this.sourceCode = sourceCode;
// Detect or use specified language
const language = options.language
? (0, languages_1.getLanguageByName)(options.language)
: (0, languages_1.detectLanguage)(sourceCode);
if (!language) {
const message = options.language
? `Unknown language: ${options.language}`
: 'Could not detect language. Please specify language option.';
throw new errors_1.LanguageError(message, sourceCode.slice(0, 100));
}
try {
this.parser.setLanguage(language.parser);
this.tree = this.parser.parse(sourceCode);
// Don't throw on syntax errors - tree-sitter can handle partial parsing
// Users can check tree.root.hasError if they want to know about errors
// Robust rootNode initialization with retry mechanism for CI environments
const rootNode = this.getRootNodeWithRetry(this.tree, 3);
this.root = new node_wrapper_1.TreeNode(rootNode, sourceCode);
}
catch (error) {
if (error instanceof errors_1.ParseError || error instanceof errors_1.LanguageError)
throw error;
throw new errors_1.ParseError(`Failed to parse: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Robust rootNode getter with retry mechanism to handle race conditions in CI environments
* Addresses the known issue where tree.rootNode can be undefined in concurrent testing scenarios
*/
getRootNodeWithRetry(tree, maxRetries = 3) {
let attempts = 0;
while (attempts < maxRetries) {
const rootNode = tree.rootNode;
if (typeof rootNode?.type !== 'undefined') {
return rootNode;
}
attempts++;
if (attempts < maxRetries) {
// Short delay to allow native binding to stabilize
// This addresses the race condition documented in tree-sitter/node-tree-sitter#181
const delay = Math.min(10 * attempts, 50); // Progressive delay: 10ms, 20ms, 50ms
// Use synchronous delay to avoid async complications in constructor
const start = Date.now();
while (Date.now() - start < delay) {
// Busy wait for very short delays
}
}
}
throw new errors_1.ParseError(`Failed to get valid rootNode after ${maxRetries} attempts. This is likely caused by a race condition in tree-sitter native bindings. ` +
'Consider running tests serially or using a Jest moduleNameMapper workaround for tree-sitter.');
}
findFirstError(node) {
if (node.type === 'ERROR')
return node;
for (const child of node.children) {
const error = this.findFirstError(child);
if (error)
return error;
}
return null;
}
// Delegate common methods to root
find(pattern) {
return this.root.find(pattern);
}
findAll(pattern) {
return this.root.findAll(pattern);
}
functions() {
return this.root.functions();
}
classes() {
return this.root.classes();
}
imports() {
return this.root.imports();
}
variables() {
return this.root.variables();
}
comments() {
return this.root.comments();
}
exports() {
return this.root.exports();
}
// JSX helpers
jsxComponents() {
return this.root.jsxComponents();
}
jsxProps(componentName) {
return this.root.jsxProps(componentName);
}
hooks() {
return this.root.hooks();
}
// Visitor pattern
visit(visitor) {
this.root.visit(visitor);
}
// Scope analysis
analyzeScopes() {
const analyzer = new visitor_1.ScopeAnalyzer();
analyzer.analyze(this.root);
return analyzer;
}
// Find node at position
nodeAt(line, column) {
return this.root.nodeAt(line, column);
}
// Enhanced analysis methods that return structured data
/**
* Get detailed function information with parameters and body ranges
*/
getFunctionDetails() {
const functions = this.functions();
return functions.map(fn => ({
name: fn.name,
type: fn.type,
async: fn.isAsync(),
parameters: fn.extractParameters(),
startLine: fn.line,
endLine: fn.endLine,
text: fn.text,
bodyRange: fn.getBodyRange() ?? undefined,
}));
}
/**
* Get detailed class information with methods and body ranges
*/
getClassDetails() {
const classes = this.classes();
return classes.map(cls => {
const methods = cls.findAll('method_definition');
const methodDetails = methods.map(method => ({
name: method.name,
type: method.type,
async: method.isAsync(),
parameters: method.extractParameters(),
startLine: method.line,
endLine: method.endLine,
text: method.text,
bodyRange: method.getBodyRange() ?? undefined,
}));
return {
name: cls.name,
methods: methodDetails,
properties: cls.findAll('field_definition').map(prop => prop.name ?? 'unknown'),
startLine: cls.line,
endLine: cls.endLine,
text: cls.text,
bodyRange: cls.getBodyRange() ?? undefined,
};
});
}
// Transform API
transform() {
return new transform_1.Transform(this.root, this.sourceCode);
}
}
exports.TreeHugger = TreeHugger;
// Main entry point functions
function parse(filenameOrCode, options) {
let sourceCode;
// Check if it's a file path or raw code
if (filenameOrCode.endsWith('.js') ||
filenameOrCode.endsWith('.ts') ||
filenameOrCode.endsWith('.jsx') ||
filenameOrCode.endsWith('.tsx') ||
filenameOrCode.endsWith('.mjs') ||
filenameOrCode.endsWith('.cjs')) {
try {
sourceCode = (0, fs_1.readFileSync)(filenameOrCode, 'utf-8');
// Use filename for better language detection
if (!options?.language) {
const lang = (0, languages_1.detectLanguage)(filenameOrCode);
if (lang) {
options = { ...options, language: lang.name };
}
}
}
catch {
// If file read fails, treat as source code
sourceCode = filenameOrCode;
}
}
else {
sourceCode = filenameOrCode;
}
return new TreeHugger(sourceCode, options);
}
var node_wrapper_2 = require("./node-wrapper");
Object.defineProperty(exports, "TreeNode", { enumerable: true, get: function () { return node_wrapper_2.TreeNode; } });
__exportStar(require("./types"), exports);
//# sourceMappingURL=tree-hugger.js.map