code-graph-generator
Version:
Generate Json Object of code that can be used to generate code-graphs for JavaScript/TypeScript/Range projects
422 lines • 17 kB
JavaScript
;
// // src/analyzer/relationship-analyzer.ts
// import { CodeGraph, FileGraph, FunctionGraph, TypeGraph } from '../types/interfaces';
// import path from 'path';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeRelationships = analyzeRelationships;
const path_1 = __importDefault(require("path"));
const ast_utils_1 = require("../utils/ast-utils");
function analyzeRelationships(codeGraph) {
const functionMap = buildFunctionMap(codeGraph);
const typeMap = buildTypeMap(codeGraph);
// Analyze package dependencies first
analyzePackageDependencies(codeGraph);
const enhancedGraph = {
...codeGraph,
packages: codeGraph.packages.map(pkg => ({
...pkg,
files: pkg.files.map(file => enhanceFileRelationships(file, codeGraph, functionMap, typeMap))
}))
};
return enhancedGraph;
}
function analyzePackageDependencies(codeGraph) {
const packageMap = new Map();
// Build a map of packages by name
codeGraph.packages.forEach(pkg => {
packageMap.set(pkg.name, pkg);
});
// Find files that import from other packages
codeGraph.packages.forEach(pkg => {
const dependencies = new Set();
pkg.files.forEach(file => {
file.dependencies.forEach(dep => {
// Skip non-relative imports (node modules)
if (!dep.startsWith('.'))
return;
// Resolve the import path
const importPath = resolveImportPath(file.path, dep);
const targetFile = findFileByPath(codeGraph, importPath);
if (targetFile) {
const targetPkg = codeGraph.packages.find(p => p.files.some(f => f.path === targetFile.path));
if (targetPkg && targetPkg.name !== pkg.name) {
dependencies.add(targetPkg.name);
}
}
});
});
// Update package dependencies
pkg.dependencies = Array.from(dependencies);
});
}
function enhanceFileRelationships(file, codeGraph, functionMap, typeMap) {
// First, check if the file needs casing fixed
const sourceFileName = path_1.default.basename(file.path);
const correctCaseFile = findCorrectCaseFile(file.path, codeGraph);
if (correctCaseFile && correctCaseFile.path !== file.path) {
file.path = correctCaseFile.path;
}
return {
...file,
functions: file.functions.map(fn => ({
...fn,
fileName: sourceFileName, // Ensure fileName matches the actual case
callsTo: fn.callsTo || findFunctionCalls(fn, file, codeGraph, functionMap),
calledBy: findFunctionCallers(fn, codeGraph, functionMap)
})),
componentHierarchy: file.componentHierarchy
? enhanceComponentHierarchy(file.componentHierarchy, file, codeGraph)
: (file.path.endsWith('.jsx') || file.path.endsWith('.tsx') || file.path.endsWith('.js')
? buildComponentHierarchy(file, codeGraph, functionMap)
: undefined),
detailedDependencies: file.detailedDependencies || analyzeModuleDependencies(file, codeGraph)
};
}
// Helper to find the file with correct case
function findCorrectCaseFile(filePath, codeGraph) {
const basePath = path_1.default.dirname(filePath);
const fileName = path_1.default.basename(filePath);
for (const pkg of codeGraph.packages) {
for (const file of pkg.files) {
if (path_1.default.dirname(file.path) === basePath &&
path_1.default.basename(file.path).toLowerCase() === fileName.toLowerCase()) {
return file;
}
}
}
return undefined;
}
function enhanceComponentHierarchy(hierarchy, file, codeGraph) {
// Check if the hierarchy has the renderedBy property already
const hasRenderedBy = Object.values(hierarchy).some(h => 'renderedBy' in h);
// If it already has the full structure, return it
if (hasRenderedBy)
return hierarchy;
// Otherwise, enhance it with renderedBy relationships
const enhanced = {};
// Initialize with existing renders
Object.keys(hierarchy).forEach(component => {
enhanced[component] = {
renders: hierarchy[component].renders || [],
renderedBy: []
};
});
// Add renderedBy relationships
Object.keys(enhanced).forEach(component => {
const renders = enhanced[component].renders;
renders.forEach(rendered => {
if (enhanced[rendered]) {
enhanced[rendered].renderedBy.push(component);
}
else {
enhanced[rendered] = {
renders: [],
renderedBy: [component]
};
}
});
});
return enhanced;
}
function buildFunctionMap(codeGraph) {
const functionMap = new Map();
codeGraph.packages.forEach(pkg => {
pkg.files.forEach(file => {
file.functions.forEach(fn => {
if (fn.name !== 'root') {
// Store with qualified name (file:function)
const qualifiedName = `${file.path}:${fn.name}`;
functionMap.set(qualifiedName, { function: fn, file });
// Also store by function name for simpler lookups
// But only if there's no collision
if (!functionMap.has(fn.name)) {
functionMap.set(fn.name, { function: fn, file });
}
}
});
});
});
return functionMap;
}
function buildTypeMap(codeGraph) {
const typeMap = new Map();
codeGraph.packages.forEach(pkg => {
pkg.files.forEach(file => {
file.types.forEach(type => {
// Store with qualified name (file:type)
const qualifiedName = `${file.path}:${type.name}`;
typeMap.set(qualifiedName, { type, file });
// Also store by type name for simpler lookups
// But only if there's no collision
if (!typeMap.has(type.name)) {
typeMap.set(type.name, { type, file });
}
});
});
});
return typeMap;
}
function findFunctionCalls(fn, file, codeGraph, functionMap) {
// If the function already has callsTo from the parser, use it
if (fn.callsTo && fn.callsTo.length > 0) {
return fn.callsTo;
}
const calls = [];
// For root function, extract from dependencies
if (fn.name === 'root') {
fn.dependencies.forEach(dep => {
const importedName = Object.keys(dep)[0];
if (functionMap.has(importedName)) {
calls.push(importedName);
}
});
return calls;
}
// For regular functions, look at dependencies
if (fn.dependencies) {
fn.dependencies.forEach(dep => {
const importedName = Object.keys(dep)[0];
if (functionMap.has(importedName)) {
calls.push(importedName);
}
});
}
// Only add local functions that are likely to be called
const localFunctions = file.functions
.filter(otherFn => otherFn.name !== fn.name && otherFn.name !== 'root');
// For each local function, check if it's referenced in the function
localFunctions.forEach(localFn => {
// Here we could have more sophisticated detection
// But for now we'll trust the callsTo property from the parser
if (fn.callsTo && fn.callsTo.includes(localFn.name)) {
calls.push(localFn.name);
}
});
// Find calls to imported functions from other files
file.dependencies.forEach(dep => {
const depFile = findFileByPath(codeGraph, resolveImportPath(file.path, dep));
if (depFile) {
depFile.exports.forEach(exportName => {
const exportedFn = depFile.functions.find(f => f.name === exportName);
if (exportedFn) {
// Check if this function calls the exported one
const depKey = `${exportName}`;
if (fn.dependencies && fn.dependencies.some(d => Object.keys(d)[0] === depKey)) {
calls.push(exportName);
}
// Also check callsTo property
if (fn.callsTo && fn.callsTo.includes(exportName)) {
calls.push(exportName);
}
}
});
}
});
// Deduplicate
return [...new Set(calls)];
}
function findFunctionCallers(fn, codeGraph, functionMap) {
const callers = [];
if (fn.name === 'root') {
return callers; // root is never called
}
codeGraph.packages.forEach(pkg => {
pkg.files.forEach(file => {
const isExported = file.exports.includes(fn.name);
const fileContainsFn = file.path === fn.fileName ||
file.functions.some(f => f.name === fn.name);
file.functions.forEach(potentialCaller => {
// Skip if it's the same function or root
if (potentialCaller.name === fn.name || potentialCaller.name === 'root') {
return;
}
// Check if there's a direct call reference
if (potentialCaller.callsTo && potentialCaller.callsTo.includes(fn.name)) {
callers.push(`${file.path}:${potentialCaller.name}`);
return;
}
// Check dependencies for imported function references
if (isExported || fileContainsFn) {
if (potentialCaller.dependencies && potentialCaller.dependencies.length > 0) {
const callsFunction = potentialCaller.dependencies.some(dep => {
const depName = Object.keys(dep)[0];
return depName === fn.name;
});
if (callsFunction) {
callers.push(`${file.path}:${potentialCaller.name}`);
}
}
}
});
});
});
return callers;
}
function buildComponentHierarchy(file, codeGraph, functionMap) {
const componentHierarchy = {};
// Find components by looking for functions that start with capital letter
// and (ideally) have a React component indicator
const componentFunctions = file.functions.filter(fn => {
// Either it's marked as a component, or it has a capital first letter
const isComponent = fn.name.includes('(React Component)') ||
(fn.name !== 'root' && /^[A-Z]/.test(fn.name));
return isComponent;
});
// Initialize hierarchy
componentFunctions.forEach(component => {
// Clean up name (remove React Component suffix)
const cleanName = component.name.replace(' (React Component)', '');
componentHierarchy[cleanName] = {
renders: [],
renderedBy: []
};
});
// Find imported components
const importedComponents = {};
file.dependencies.forEach(dep => {
const depFile = findFileByPath(codeGraph, resolveImportPath(file.path, dep));
if (!depFile)
return;
depFile.exports.forEach(exportName => {
// Likely a component if capitalized
if (/^[A-Z]/.test(exportName)) {
importedComponents[exportName] = depFile.path;
}
});
});
// Now determine actual component relationships based on:
// 1. Function calls
// 2. JSX usage (if available)
componentFunctions.forEach(component => {
const cleanName = component.name.replace(' (React Component)', '');
// Use callsTo if available
if (component.callsTo && component.callsTo.length > 0) {
component.callsTo.forEach(calledName => {
// Filter to components (start with capital letter)
if (/^[A-Z]/.test(calledName)) {
componentHierarchy[cleanName].renders.push(calledName);
}
});
}
// Look for dependencies that match imports
if (component.dependencies) {
component.dependencies.forEach(dep => {
const depName = Object.keys(dep)[0];
if (importedComponents[depName]) {
componentHierarchy[cleanName].renders.push(depName);
}
});
}
// In the absence of clear JSX analysis, we can use component
// hierarchy data from React parser if available
if (file.componentHierarchy && file.componentHierarchy[cleanName]) {
const existingRenders = file.componentHierarchy[cleanName].renders || [];
componentHierarchy[cleanName].renders = [
...new Set([...componentHierarchy[cleanName].renders, ...existingRenders])
];
}
});
// Build the renderedBy inverse relationships
Object.keys(componentHierarchy).forEach(component => {
const renders = componentHierarchy[component].renders;
renders.forEach(rendered => {
if (componentHierarchy[rendered]) {
if (!componentHierarchy[rendered].renderedBy.includes(component)) {
componentHierarchy[rendered].renderedBy.push(component);
}
}
else {
// Add entry for imported component that's rendered
componentHierarchy[rendered] = {
renders: [],
renderedBy: [component]
};
}
});
});
return componentHierarchy;
}
function analyzeModuleDependencies(file, codeGraph) {
const detailedDependencies = [];
// If the file already has detailed dependencies, use them as a starting point
if (file.detailedDependencies && file.detailedDependencies.length > 0) {
return file.detailedDependencies;
}
file.dependencies.forEach(dep => {
const imports = [];
// Check all functions for dependencies on this module
file.functions.forEach(fn => {
if (fn.dependencies) {
fn.dependencies.forEach(fnDep => {
const importName = Object.keys(fnDep)[0];
const importSource = fnDep[importName];
if (importSource === dep) {
imports.push(importName);
}
});
}
});
// Create a record for this dependency
detailedDependencies.push({
module: dep,
imports: [...new Set(imports)]
});
});
return detailedDependencies;
}
function resolveImportPath(importerPath, importPath) {
// Handle absolute imports
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
return importPath;
}
// Resolve relative path
const importerDir = path_1.default.dirname(importerPath);
let resolvedPath = path_1.default.normalize(path_1.default.join(importerDir, importPath)).replace(/\\/g, '/');
// If the path doesn't have an extension, return it as is
if (!path_1.default.extname(resolvedPath)) {
return resolvedPath;
}
return resolvedPath;
}
function findFileByPath(codeGraph, filePath) {
const normalizedPath = (0, ast_utils_1.normalizePath)(filePath);
// Try exact match first
for (const pkg of codeGraph.packages) {
for (const file of pkg.files) {
if (file.path === normalizedPath) {
return file;
}
}
}
// Try case-insensitive match
const lowerPath = normalizedPath.toLowerCase();
for (const pkg of codeGraph.packages) {
for (const file of pkg.files) {
if (file.path.toLowerCase() === lowerPath) {
return file;
}
}
}
// Try without extension
const pathWithoutExt = normalizedPath.replace(/\.[^/.]+$/, '');
for (const pkg of codeGraph.packages) {
for (const file of pkg.files) {
const filePathNoExt = file.path.replace(/\.[^/.]+$/, '');
if (filePathNoExt === pathWithoutExt) {
return file;
}
// Check common extensions
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
for (const ext of extensions) {
if (file.path === `${pathWithoutExt}${ext}`) {
return file;
}
}
}
}
return undefined;
}
//# sourceMappingURL=relationship-analyzer.js.map