UNPKG

tree-hugger-js

Version:

A friendly tree-sitter wrapper for JavaScript and TypeScript

227 lines 8.58 kB
"use strict"; 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