UNPKG

code-graph-generator

Version:

Generate Json Object of code that can be used to generate code-graphs for JavaScript/TypeScript/Range projects

990 lines 71.6 kB
"use strict"; // // src/parser/react-parser.js // import { parse } from '@babel/parser'; // import path from 'path'; // import { FileGraph, FileParser, FunctionGraph, VariableGraph } from '../types/interfaces'; // import { getStartLine, countLines, lightTraverse, normalizePath } from '../utils/ast-utils'; // import { logger } from '../utils/file-utils'; // import { createJsParser } from './js-parser'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createReactParser = createReactParser; // export function createReactParser(): FileParser { // const jsParser = createJsParser(); // return { // async parse(filePath: string, content: string): Promise<FileGraph> { // logger.debug(`React Parser: parsing ${filePath}`); // const fileGraph = await jsParser.parse(filePath, content); // try { // const ast = parse(content, { // sourceType: 'module', // plugins: [ // 'typescript', // 'classProperties', // 'dynamicImport', // ['jsx', { runtime: 'automatic', importSource: 'react' }] // ] as any, // errorRecovery: true, // }); // extractReactImports(ast, fileGraph); // extractReactComponents(ast, fileGraph, filePath, content); // extractReactHooks(ast, fileGraph, filePath); // return fileGraph; // } catch (error) { // logger.warn(`React-specific parsing failed for ${filePath}, using JS parser result:`, error); // return fileGraph; // } // } // }; // } // function extractReactImports(ast: any, fileGraph: FileGraph): void { // const visitors = { // ImportDeclaration: (node: any) => { // const source = node.source.value; // // Process all imports, not just react ones // const importNames: string[] = []; // node.specifiers.forEach((specifier: any) => { // if (specifier.type === 'ImportDefaultSpecifier') { // importNames.push('default'); // } // else if (specifier.type === 'ImportSpecifier') { // if (specifier.imported && specifier.imported.name) { // importNames.push(specifier.imported.name); // } else if (specifier.local && specifier.local.name) { // // Handle the case where imported name might not be available // importNames.push(specifier.local.name); // } // } // }); // fileGraph.detailedDependencies = fileGraph.detailedDependencies || []; // const existingDep = fileGraph.detailedDependencies.find(dep => dep.module === source); // if (existingDep) { // existingDep.imports = [...new Set([...existingDep.imports, ...importNames])]; // } else { // fileGraph.detailedDependencies.push({ // module: source, // imports: importNames // }); // } // } // }; // lightTraverse(ast, visitors); // } // function extractReactComponents(ast: any, fileGraph: FileGraph, filePath: string, content: string): void { // const isReactComponent = (node: any): boolean => { // if (node.body && node.body.type === 'BlockStatement') { // let hasJsx = false; // let usesHooks = false; // const visitor = { // ReturnStatement: (returnNode: any) => { // if (returnNode.argument && // (returnNode.argument.type === 'JSXElement' || // returnNode.argument.type === 'JSXFragment')) { // hasJsx = true; // } // }, // CallExpression: (callNode: any) => { // if (callNode.callee && // callNode.callee.type === 'Identifier' && // callNode.callee.name.startsWith('use')) { // usesHooks = true; // } // } // }; // lightTraverse(node.body, visitor); // return hasJsx || usesHooks; // } // if (node.body && // (node.body.type === 'JSXElement' || // node.body.type === 'JSXFragment')) { // return true; // } // return false; // }; // const parentMap = new Map(); // buildParentMap(ast, null, parentMap); // const getVariableDeclaration = (node: any): any => { // let current = node; // while (current && parentMap.has(current)) { // const parent = parentMap.get(current); // if (parent && parent.type === 'VariableDeclarator' && parent.id) { // return parent; // } // current = parent; // } // return null; // }; // const componentNames = new Set<string>(); // const visitors = { // ArrowFunctionExpression: (node: any) => { // if (isReactComponent(node)) { // const parentDecl = getVariableDeclaration(node); // if (parentDecl && parentDecl.id && parentDecl.id.type === 'Identifier') { // if (isTopLevel(parentDecl, parentMap)) { // const name = parentDecl.id.name; // const startLine = getStartLine(node); // const componentGraph: FunctionGraph = { // fileName: path.basename(filePath), // name: `${name} (React Component)`, // referencedIn: [normalizePath(filePath)], // startLine, // length: node.body ? countLines(content.substring( // node.body.start || 0, // node.body.end || content.length // )) : 0, // dependencies: [], // types: ['React.FC'], // }; // extractComponentProps(node, componentGraph); // if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { // fileGraph.functions.push(componentGraph); // componentNames.add(name); // Track component name for hierarchy // } // } // } // } // }, // FunctionDeclaration: (node: any) => { // if (node.id) { // const name = node.id.name; // const startLine = getStartLine(node); // // For component functions (top-level or nested) // if (isReactComponent(node)) { // // Top-level components get both regular and component versions // if (isTopLevel(node, parentMap)) { // // Add regular function version // if (!fileGraph.functions.some(fn => fn.name === name)) { // const regularFunction: FunctionGraph = { // fileName: path.basename(filePath), // name: name, // referencedIn: [normalizePath(filePath)], // startLine, // length: node.body ? countLines(content.substring( // node.body.start || 0, // node.body.end || content.length // )) : 0, // dependencies: [], // types: [], // }; // fileGraph.functions.push(regularFunction); // } // // Add React component version // const componentGraph: FunctionGraph = { // fileName: path.basename(filePath), // name: `${name} (React Component)`, // referencedIn: [normalizePath(filePath)], // startLine, // length: node.body ? countLines(content.substring( // node.body.start || 0, // node.body.end || content.length // )) : 0, // dependencies: [], // types: ['React.FC'], // }; // extractComponentProps(node, componentGraph); // if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { // fileGraph.functions.push(componentGraph); // componentNames.add(name); // Track component name for hierarchy // } // } // } // // For nested functions like handleClick // else if (!isTopLevel(node, parentMap) && isInComponent(node, parentMap)) { // const functionGraph: FunctionGraph = { // fileName: path.basename(filePath), // name, // referencedIn: [normalizePath(filePath)], // startLine, // length: node.body ? countLines(content.substring( // node.body.start || 0, // node.body.end || content.length // )) : 0, // dependencies: [], // types: inferFunctionReturnType(node), // callsTo: findFunctionCalls(node, content, ast), // }; // if (!fileGraph.functions.some(fn => fn.name === name)) { // fileGraph.functions.push(functionGraph); // } // } // } // }, // VariableDeclaration: (node: any) => { // if (isTopLevel(node, parentMap)) { // node.declarations.forEach((decl: any) => { // if (decl.init && // (decl.init.type === 'FunctionExpression' || // decl.init.type === 'ArrowFunctionExpression') && // isReactComponent(decl.init) && // decl.id && // decl.id.type === 'Identifier') { // const name = decl.id.name; // const startLine = getStartLine(decl.init); // const componentGraph: FunctionGraph = { // fileName: path.basename(filePath), // name: `${name} (React Component)`, // referencedIn: [normalizePath(filePath)], // startLine, // length: decl.init.body ? countLines(content.substring( // decl.init.body.start || 0, // decl.init.body.end || content.length // )) : 0, // dependencies: [], // types: ['React.FC'], // }; // extractComponentProps(decl.init, componentGraph); // if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { // fileGraph.functions.push(componentGraph); // componentNames.add(name); // Track component name for hierarchy // } // } // }); // } // // Also check for arrow functions inside components // else if (isInComponent(node, parentMap)) { // node.declarations.forEach((decl: any) => { // if (decl.id?.type === 'Identifier' && // decl.init?.type === 'ArrowFunctionExpression') { // const name = decl.id.name; // const startLine = getStartLine(decl.init); // const functionGraph: FunctionGraph = { // fileName: path.basename(filePath), // name, // referencedIn: [normalizePath(filePath)], // startLine, // length: decl.init.body ? countLines(content.substring( // decl.init.body.start || 0, // decl.init.body.end || content.length // )) : 0, // dependencies: [], // types: inferFunctionReturnType(decl.init), // callsTo: findFunctionCalls(decl.init, content, ast), // }; // if (!fileGraph.functions.some(fn => fn.name === name)) { // fileGraph.functions.push(functionGraph); // } // } // }); // } // } // }; // lightTraverse(ast, visitors); // // Build component hierarchy with the tracked component names // if (componentNames.size > 0) { // const componentHierarchy = buildComponentHierarchy(ast, fileGraph, componentNames, parentMap); // fileGraph.componentHierarchy = componentHierarchy; // } else { // // If no components were found but file has JSX extension, check for unnamed/inline components // if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx') || filePath.endsWith('.js')) { // const functionNames = fileGraph.functions // .filter(fn => fn.name !== 'root') // .map(fn => fn.name.replace(' (React Component)', '')); // // Create a default hierarchy for any named functions that might be components // if (functionNames.length > 0) { // const defaultHierarchy: Record<string, { renders: string[], renderedBy: string[] }> = {}; // functionNames.forEach(name => { // defaultHierarchy[name] = { renders: [], renderedBy: [] }; // }); // fileGraph.componentHierarchy = defaultHierarchy; // } // } // } // } // function extractComponentProps(node: any, component: FunctionGraph): void { // if (node.params && node.params.length > 0) { // const propsParam = node.params[0]; // if (propsParam.type === 'ObjectPattern' && propsParam.properties) { // propsParam.properties.forEach((prop: any) => { // if (prop.key && prop.key.name) { // component.dependencies.push({ [`prop:${prop.key.name}`]: 'component-props' }); // } // }); // } // else if (propsParam.type === 'Identifier' && propsParam.typeAnnotation) { // component.dependencies.push({ ['props']: 'component-props' }); // if (propsParam.typeAnnotation.typeAnnotation && // propsParam.typeAnnotation.typeAnnotation.type === 'TSTypeReference' && // propsParam.typeAnnotation.typeAnnotation.typeName) { // const typeName = propsParam.typeAnnotation.typeAnnotation.typeName.name; // component.types.push(`Props: ${typeName}`); // } // } // else if (propsParam.type === 'Identifier') { // component.dependencies.push({ ['props']: 'component-props' }); // } // } // } // function extractReactHooks(ast: any, fileGraph: FileGraph, filePath: string): void { // const hookVariables: Record<string, string[]> = {}; // const parentMap = new Map(); // buildParentMap(ast, null, parentMap); // // Track hooks directly // const hooksUsed = new Set<string>(); // const visitors = { // CallExpression: (node: any) => { // // Check for React hooks (functions that start with 'use') // if (node.callee?.type === 'Identifier' && // node.callee.name.startsWith('use')) { // hooksUsed.add(node.callee.name); // // For useState, attempt to find the array pattern it's assigned to // if (node.callee.name === 'useState') { // let parentNode = parentMap.get(node); // while (parentNode && parentNode.type !== 'VariableDeclarator') { // parentNode = parentMap.get(parentNode); // } // // If we found the variable declarator, extract state variables // if (parentNode?.id?.type === 'ArrayPattern' && // parentNode.id.elements?.length >= 2) { // const stateVar = parentNode.id.elements[0]; // const setterVar = parentNode.id.elements[1]; // if (stateVar?.type === 'Identifier' && setterVar?.type === 'Identifier') { // const varName = stateVar.name; // const setterName = setterVar.name; // // Create and add state variable // const stateVarGraph: VariableGraph = { // name: `${varName} (State)`, // type: 'State', // dependencies: [{ 'useState': 'react' }] // }; // // Create and add setter variable // const setterVarGraph: VariableGraph = { // name: `${setterName} (State Setter)`, // type: 'StateSetter', // dependencies: [{ [varName]: 'state-var' }] // }; // // Ensure these are always added to the file graph // if (!fileGraph.variables.some(v => v.name === stateVarGraph.name)) { // fileGraph.variables.push(stateVarGraph); // } // if (!fileGraph.variables.some(v => v.name === setterVarGraph.name)) { // fileGraph.variables.push(setterVarGraph); // } // } // } // } // } // }, // VariableDeclaration: (node: any) => { // node.declarations.forEach((decl: any) => { // if (decl.id?.type === 'ArrayPattern' && // decl.init?.type === 'CallExpression' && // decl.init.callee?.type === 'Identifier' && // decl.init.callee.name.startsWith('use')) { // const hookName = decl.init.callee.name; // hooksUsed.add(hookName); // if (hookName === 'useState' && decl.id.elements?.length >= 2) { // const stateVar = decl.id.elements[0]; // const setterVar = decl.id.elements[1]; // if (stateVar?.type === 'Identifier') { // const varName = stateVar.name; // const stateVarGraph: VariableGraph = { // name: `${varName} (State)`, // type: 'State', // dependencies: [{ 'useState': 'react' }] // }; // // Force add even if it already exists // const existingIndex = fileGraph.variables.findIndex(v => v.name === stateVarGraph.name); // if (existingIndex >= 0) { // fileGraph.variables[existingIndex] = stateVarGraph; // } else { // fileGraph.variables.push(stateVarGraph); // } // if (setterVar?.type === 'Identifier') { // const setterName = setterVar.name; // hookVariables[varName] = [setterName]; // const setterVarGraph: VariableGraph = { // name: `${setterName} (State Setter)`, // type: 'StateSetter', // dependencies: [{ [varName]: 'state-var' }] // }; // // Force add even if it already exists // const existingSetterIndex = fileGraph.variables.findIndex(v => v.name === setterVarGraph.name); // if (existingSetterIndex >= 0) { // fileGraph.variables[existingSetterIndex] = setterVarGraph; // } else { // fileGraph.variables.push(setterVarGraph); // } // } // } // } // } // }); // } // }; // // Use our custom traversal to ensure we find all hooks // lightTraverseWithParents(ast, visitors); // // Add the hooks used as variables // hooksUsed.forEach(hookName => { // const hookVar: VariableGraph = { // name: hookName, // type: 'React Hook', // dependencies: [{ 'react': 'module' }] // }; // // Avoid duplication // if (!fileGraph.variables.some(v => v.name === hookVar.name)) { // fileGraph.variables.push(hookVar); // } // }); // } // // Helper to build parent map for hook detection // function buildParentMap(node: any, parent: any, map: Map<any, any> = new Map()): Map<any, any> { // if (!node || typeof node !== 'object') return map; // map.set(node, parent); // for (const key in node) { // if (node.hasOwnProperty(key) && key !== 'loc' && key !== 'range' && key !== 'parent') { // const child = node[key]; // if (child && typeof child === 'object') { // if (Array.isArray(child)) { // child.forEach(item => buildParentMap(item, node, map)); // } else { // buildParentMap(child, node, map); // } // } // } // } // return map; // } // // New helper function to check if a node is inside a React component // function isInComponent(node: any, parentMap: Map<any, any>): boolean { // let current = node; // while (current && parentMap.has(current)) { // const parent = parentMap.get(current); // // Check if this is a component function // if ((parent.type === 'FunctionDeclaration' || // parent.type === 'ArrowFunctionExpression' || // parent.type === 'FunctionExpression')) { // // Component names usually start with capital letter // if (parent.type === 'FunctionDeclaration' && parent.id?.name) { // if (/^[A-Z]/.test(parent.id.name)) { // return true; // } // } // // Check variable name for function expressions and arrow functions // let current = parent; // while (current && parentMap.has(current)) { // const p = parentMap.get(current); // if (p.type === 'VariableDeclarator' && p.id?.type === 'Identifier') { // if (/^[A-Z]/.test(p.id.name)) { // return true; // } // break; // } // current = p; // } // // Check if the function returns JSX // if (parent.body && parent.body.type === 'BlockStatement') { // let hasJsx = false; // const visitor = { // ReturnStatement: (returnNode: any) => { // if (returnNode.argument && // (returnNode.argument.type === 'JSXElement' || // returnNode.argument.type === 'JSXFragment')) { // hasJsx = true; // } // } // }; // lightTraverse(parent.body, visitor); // if (hasJsx) { // return true; // } // } // } // current = parent; // } // return false; // } // // Helper function to check if a node is at the top level // function isTopLevel(node: any, parentMap: Map<any, any>): boolean { // let current = node; // while (current && parentMap.has(current)) { // const parent = parentMap.get(current); // // If we reach the Program node directly, it's top level // if (parent && parent.type === 'Program') { // return true; // } // // If we reach a FunctionDeclaration or other scope creator, // // and it's not directly under Program, it's not top level // if ( // parent && // (parent.type === 'FunctionDeclaration' || // parent.type === 'ArrowFunctionExpression' || // parent.type === 'FunctionExpression' || // parent.type === 'BlockStatement') // ) { // return false; // } // current = parent; // } // return false; // } // // Define the lightTraverseWithParents function // function lightTraverseWithParents(ast: any, visitors: Record<string, Function>, visited: Set<any> = new Set()): void { // function visit(node: any, parent: any = null, grandparent: any = null) { // if (!node || typeof node !== 'object' || visited.has(node)) return; // visited.add(node); // if (node.type && visitors[node.type]) { // visitors[node.type](node, parent, grandparent); // } // for (const key of Object.keys(node)) { // if (key === 'loc' || key === 'range' || key === 'parent') continue; // const child = node[key]; // if (!child || typeof child !== 'object') continue; // if (Array.isArray(child)) { // for (const item of child) { // visit(item, node, parent); // } // } else { // visit(child, node, parent); // } // } // } // visit(ast); // } // // Improved component hierarchy builder // function buildComponentHierarchy( // ast: any, // fileGraph: FileGraph, // componentNames: Set<string>, // parentMap: Map<any, any> // ): Record<string, { renders: string[], renderedBy: string[] }> { // const componentHierarchy: Record<string, { renders: string[], renderedBy: string[] }> = {}; // // Initialize hierarchy for all components // componentNames.forEach(name => { // componentHierarchy[name] = { // renders: [], // renderedBy: [] // }; // }); // // Function to find containing component function for a JSX element // const findContainingComponent = (node: any): string | null => { // let current = node; // while (current && parentMap.has(current)) { // const parent = parentMap.get(current); // // Check if it's a function or arrow function // if ((parent.type === 'FunctionDeclaration' || // parent.type === 'ArrowFunctionExpression' || // parent.type === 'FunctionExpression')) { // // Get function name // let name = null; // if (parent.type === 'FunctionDeclaration' && parent.id) { // name = parent.id.name; // } else { // // For arrow functions and function expressions, find variable declaration // let current = parent; // while (current && parentMap.has(current)) { // const p = parentMap.get(current); // if (p.type === 'VariableDeclarator' && p.id && p.id.type === 'Identifier') { // name = p.id.name; // break; // } // current = p; // } // } // if (name && componentNames.has(name)) { // return name; // } // } // current = parent; // } // return null; // }; // // Find JSX elements and track relationships // const visitors = { // JSXElement: (node: any) => { // if (node.openingElement && node.openingElement.name) { // const elementName = // node.openingElement.name.type === 'JSXIdentifier' // ? node.openingElement.name.name // : null; // // Only process component references (capitalized names) // if (elementName && elementName[0] === elementName[0].toUpperCase()) { // // Find which component contains this JSX // const parentComponent = findContainingComponent(node); // if (parentComponent && componentHierarchy[parentComponent]) { // // Add element as rendered by this component // if (!componentHierarchy[parentComponent].renders.includes(elementName)) { // componentHierarchy[parentComponent].renders.push(elementName); // } // // Update rendered-by relationship if element is a tracked component // if (componentHierarchy[elementName]) { // if (!componentHierarchy[elementName].renderedBy.includes(parentComponent)) { // componentHierarchy[elementName].renderedBy.push(parentComponent); // } // } // } // } // } // } // }; // // Traverse the AST to find all JSX elements // lightTraverse(ast, visitors); // return componentHierarchy; // } // // Helper functions // function inferFunctionReturnType(node: any): string[] { // if (node.returnType && node.returnType.typeAnnotation) { // const typeAnnotation = node.returnType.typeAnnotation; // if (typeAnnotation.type === 'TSNumberKeyword') { // return ['number']; // } else if (typeAnnotation.type === 'TSStringKeyword') { // return ['string']; // } else if (typeAnnotation.type === 'TSBooleanKeyword') { // return ['boolean']; // } // } // return []; // } // function findFunctionCalls(node: any, content: string, ast: any): string[] { // const calls: string[] = []; // const importReferences = new Map(); // // First find all variable declarations that reference imports // const scopeVisitor = { // VariableDeclaration: (node: any) => { // node.declarations.forEach((decl: any) => { // if (decl.id?.type === 'Identifier' && // decl.init?.type === 'Identifier') { // // Track variables that reference other identifiers // importReferences.set(decl.id.name, decl.init.name); // } // }); // } // }; // // Then find all function calls // const callVisitor = { // CallExpression: (node: any) => { // if (node.callee?.type === 'Identifier') { // const calleeName = node.callee.name; // // Check if this is a direct call or via a reference // const actualName = importReferences.get(calleeName) || calleeName; // calls.push(actualName); // } // else if (node.callee?.type === 'MemberExpression' && // node.callee.property?.type === 'Identifier') { // // For object method calls like obj.method() // if (node.callee.object?.type === 'Identifier') { // // If we can identify the object, use "object.method" format // calls.push(`${node.callee.object.name}.${node.callee.property.name}`); // } else { // // Otherwise just use the method name // calls.push(node.callee.property.name); // } // } // } // }; // // First find variables, then analyze calls to get proper context // if (node.body) { // lightTraverse(node.body, scopeVisitor); // lightTraverse(node.body, callVisitor); // } // return calls; // } // src/parser/react-parser.js const parser_1 = require("@babel/parser"); const path_1 = __importDefault(require("path")); const ast_utils_1 = require("../utils/ast-utils"); const file_utils_1 = require("../utils/file-utils"); const js_parser_1 = require("./js-parser"); function createReactParser() { const jsParser = (0, js_parser_1.createJsParser)(); return { async parse(filePath, content) { file_utils_1.logger.debug(`React Parser: parsing ${filePath}`); // Preserve original case in file path const normalizedPath = (0, ast_utils_1.normalizePath)(filePath); const fileGraph = await jsParser.parse(normalizedPath, content); try { const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: [ 'typescript', 'classProperties', 'dynamicImport', ['jsx', { runtime: 'automatic', importSource: 'react' }] ], errorRecovery: true, }); // Use the same parent map for all traversals for consistency const parentMap = buildParentMap(ast, null); extractReactImports(ast, fileGraph); extractReactComponents(ast, fileGraph, normalizedPath, content, parentMap); extractReactHooks(ast, fileGraph, normalizedPath, parentMap); return fileGraph; } catch (error) { file_utils_1.logger.warn(`React-specific parsing failed for ${normalizedPath}, using JS parser result:`, error); return fileGraph; } } }; } function extractReactImports(ast, fileGraph) { const visitors = { ImportDeclaration: (node) => { const source = node.source.value; // Process all imports, not just react ones const importNames = []; node.specifiers.forEach((specifier) => { if (specifier.type === 'ImportDefaultSpecifier') { importNames.push('default'); // Also add the local name of default imports if (specifier.local?.name) { importNames.push(specifier.local.name); } } else if (specifier.type === 'ImportSpecifier') { if (specifier.imported && specifier.imported.name) { importNames.push(specifier.imported.name); } if (specifier.local && specifier.local.name) { // Handle the case where imported name might not be available importNames.push(specifier.local.name); } } }); fileGraph.detailedDependencies = fileGraph.detailedDependencies || []; const existingDep = fileGraph.detailedDependencies.find(dep => dep.module === source); if (existingDep) { existingDep.imports = [...new Set([...existingDep.imports, ...importNames])]; } else { fileGraph.detailedDependencies.push({ module: source, imports: importNames }); } } }; (0, ast_utils_1.lightTraverse)(ast, visitors); } function extractReactComponents(ast, fileGraph, filePath, content, parentMap) { // Check if a node is a React component const isReactComponent = (node) => { if (node.body && node.body.type === 'BlockStatement') { let hasJsx = false; let usesHooks = false; const visitor = { ReturnStatement: (returnNode) => { if (returnNode.argument && (returnNode.argument.type === 'JSXElement' || returnNode.argument.type === 'JSXFragment')) { hasJsx = true; } }, CallExpression: (callNode) => { if (callNode.callee && callNode.callee.type === 'Identifier' && callNode.callee.name.startsWith('use')) { usesHooks = true; } } }; (0, ast_utils_1.lightTraverse)(node.body, visitor); return hasJsx || usesHooks; } if (node.body && (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment')) { return true; } return false; }; // Get the variable declaration for a node const getVariableDeclaration = (node) => { let current = node; while (current && parentMap.has(current)) { const parent = parentMap.get(current); if (parent && parent.type === 'VariableDeclarator' && parent.id) { return parent; } current = parent; } return null; }; // Track component names for hierarchy building const componentNames = new Set(); // Visitors for traversing the AST const visitors = { ArrowFunctionExpression: (node) => { if (isReactComponent(node)) { const parentDecl = getVariableDeclaration(node); if (parentDecl && parentDecl.id && parentDecl.id.type === 'Identifier') { if (isTopLevel(parentDecl, parentMap)) { const name = parentDecl.id.name; const startLine = (0, ast_utils_1.getStartLine)(node); // Create regular function entry if (!fileGraph.functions.some(fn => fn.name === name)) { const regularFunction = { fileName: path_1.default.basename(filePath), name: name, referencedIn: [filePath], startLine, length: node.body ? (0, ast_utils_1.countLines)(content.substring(node.body.start || 0, node.body.end || content.length)) : 0, dependencies: [], types: [], callsTo: findFunctionCalls(node, content, ast, parentMap), }; fileGraph.functions.push(regularFunction); } // Create React component entry const componentGraph = { fileName: path_1.default.basename(filePath), name: `${name} (React Component)`, referencedIn: [filePath], startLine, length: node.body ? (0, ast_utils_1.countLines)(content.substring(node.body.start || 0, node.body.end || content.length)) : 0, dependencies: [], types: ['React.FC'], callsTo: findFunctionCalls(node, content, ast, parentMap), }; extractComponentProps(node, componentGraph); if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { fileGraph.functions.push(componentGraph); componentNames.add(name); // Track component name for hierarchy } } } } }, FunctionDeclaration: (node) => { if (node.id) { const name = node.id.name; const startLine = (0, ast_utils_1.getStartLine)(node); // For component functions (top-level or nested) if (isReactComponent(node)) { // Top-level components get both regular and component versions if (isTopLevel(node, parentMap)) { // Add regular function version if (!fileGraph.functions.some(fn => fn.name === name)) { const regularFunction = { fileName: path_1.default.basename(filePath), name: name, referencedIn: [filePath], startLine, length: node.body ? (0, ast_utils_1.countLines)(content.substring(node.body.start || 0, node.body.end || content.length)) : 0, dependencies: [], types: [], callsTo: findFunctionCalls(node, content, ast, parentMap), }; fileGraph.functions.push(regularFunction); } // Add React component version const componentGraph = { fileName: path_1.default.basename(filePath), name: `${name} (React Component)`, referencedIn: [filePath], startLine, length: node.body ? (0, ast_utils_1.countLines)(content.substring(node.body.start || 0, node.body.end || content.length)) : 0, dependencies: [], types: ['React.FC'], callsTo: findFunctionCalls(node, content, ast, parentMap), }; extractComponentProps(node, componentGraph); if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { fileGraph.functions.push(componentGraph); componentNames.add(name); // Track component name for hierarchy } } } // For nested functions like handleClick else if (!isTopLevel(node, parentMap) && isInComponent(node, parentMap)) { const functionGraph = { fileName: path_1.default.basename(filePath), name, referencedIn: [filePath], startLine, length: node.body ? (0, ast_utils_1.countLines)(content.substring(node.body.start || 0, node.body.end || content.length)) : 0, dependencies: [], types: inferFunctionReturnType(node), callsTo: findFunctionCalls(node, content, ast, parentMap), }; if (!fileGraph.functions.some(fn => fn.name === name)) { fileGraph.functions.push(functionGraph); } } } }, VariableDeclaration: (node) => { if (isTopLevel(node, parentMap)) { node.declarations.forEach((decl) => { if (decl.init && (decl.init.type === 'FunctionExpression' || decl.init.type === 'ArrowFunctionExpression') && isReactComponent(decl.init) && decl.id && decl.id.type === 'Identifier') { const name = decl.id.name; const startLine = (0, ast_utils_1.getStartLine)(decl.init); // Add regular function first if (!fileGraph.functions.some(fn => fn.name === name)) { const regularFunction = { fileName: path_1.default.basename(filePath), name: name, referencedIn: [filePath], startLine, length: decl.init.body ? (0, ast_utils_1.countLines)(content.substring(decl.init.body.start || 0, decl.init.body.end || content.length)) : 0, dependencies: [], types: [], callsTo: findFunctionCalls(decl.init, content, ast, parentMap), }; fileGraph.functions.push(regularFunction); } // Add React component version const componentGraph = { fileName: path_1.default.basename(filePath), name: `${name} (React Component)`, referencedIn: [filePath], startLine, length: decl.init.body ? (0, ast_utils_1.countLines)(content.substring(decl.init.body.start || 0, decl.init.body.end || content.length)) : 0, dependencies: [], types: ['React.FC'], callsTo: findFunctionCalls(decl.init, content, ast, parentMap), }; extractComponentProps(decl.init, componentGraph); if (!fileGraph.functions.some(fn => fn.name === componentGraph.name)) { fileGraph.functions.push(componentGraph); componentNames.add(name); // Track component name for hierarchy } } }); } // Also check for arrow functions inside components (like handleClick) else if (isInComponent(node, parentMap)) { node.declarations.forEach((decl) => { if (decl.id?.type === 'Identifier' && decl.init?.type === 'ArrowFunctionExpression') { const name = decl.id.name; const startLine = (0, ast_utils_1.getStartLine)(decl.init); // Search for called imports from parent component const dependencies = []; const importedFunctions = getImportedFunctions(fileGraph); const callsTo = findFunctionCalls(decl.init, content, ast, parentMap); // Add any called imported functions as dependencies callsTo.forEach(calledName => { if (importedFunctions[calledName]) { const importSource = importedFunctions[calledName]; dependencies.push({ [calledName]: importSource }); } }); const functionGraph = { fileName: path_1.default.basename(filePath), name, referencedIn: [filePath], startLine, length: decl.init.body ? (0, ast_utils_1.countLines)(content.substring(decl.init.body.start || 0, decl.init.body.end || content.length)) : 0, dependencies, types: inferFunctionReturnType(decl.init), callsTo, }; if (!fileGraph.functions.some(fn => fn.name === name)) { fileGraph.functions.push(functionGraph); } } }); } } }; // Traverse AST to find components and functions (0, ast_utils_1.lightTraverse)(ast, visitors); // Build component hierarchy with the tracked