sicua
Version:
A tool for analyzing project structure and dependencies
623 lines (622 loc) • 23.7 kB
JavaScript
"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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentFlowScanner = void 0;
const traverse_1 = __importDefault(require("@babel/traverse"));
const t = __importStar(require("@babel/types"));
const JSXReturnAnalyzer_1 = require("./JSXReturnAnalyzer");
const types_1 = require("../types");
const utils_1 = require("../utils");
/**
* Enhanced component flow scanner using optimized services and existing parsed files
*/
class ComponentFlowScanner {
constructor(lookupService, pathResolver, scanResult, maxDepth = 10, config) {
// Set up configuration with defaults
this.config = config || {
maxDepth,
includeExternalComponents: true,
excludePatterns: [],
onlyAnalyzeRoutes: [],
includeHtmlElements: false,
htmlElementFilter: types_1.DEFAULT_HTML_ELEMENT_FILTER,
};
this.maxDepth = this.config.maxDepth;
this.lookupService = lookupService;
this.pathResolver = pathResolver;
this.scanResult = scanResult;
// Initialize analyzers with configuration
this.jsxAnalyzer = new JSXReturnAnalyzer_1.JSXReturnAnalyzer(this.config);
// Initialize optimized state
this.analyzedComponents = new Map();
this.conditionalIds = new Set();
this.analysisInProgress = new Set();
}
/**
* Analyzes a component file and builds its complete flow tree using optimized services
*/
scanComponentFlow(filePath, depth = 0) {
// FIXED: Normalize file path to match scanResult format
const normalizedFilePath = this.normalizeFilePath(filePath);
// Create a unique key for this component analysis
const componentKey = this.createComponentKey(normalizedFilePath);
// Return cached result if already analyzed
if (this.analyzedComponents.has(componentKey)) {
return this.analyzedComponents.get(componentKey);
}
// Prevent infinite recursion
if (depth >= this.maxDepth || this.analysisInProgress.has(componentKey)) {
return null;
}
// Mark as in progress
this.analysisInProgress.add(componentKey);
try {
const fileAnalysis = this.analyzeFile(normalizedFilePath);
if (!fileAnalysis) {
return null;
}
const componentNode = this.buildComponentFlowNode(fileAnalysis, depth);
// Cache the result
this.analyzedComponents.set(componentKey, componentNode);
return componentNode;
}
catch (error) {
console.warn(`Error scanning component flow for ${normalizedFilePath}:`, error);
return null;
}
finally {
// Remove from in-progress
this.analysisInProgress.delete(componentKey);
}
}
/**
* FIXED: Normalize file path to match scanResult format (forward slashes)
*/
normalizeFilePath(filePath) {
// Convert Windows backslashes to forward slashes to match scanResult format
return filePath.replace(/\\/g, "/");
}
/**
* Analyzes multiple component files
*/
scanMultipleComponentFlows(filePaths) {
const nodes = [];
for (const filePath of filePaths) {
const node = this.scanComponentFlow(filePath);
if (node) {
nodes.push(node);
}
}
return nodes;
}
/**
* Gets detailed analysis for a specific file without building the tree
*/
getFileAnalysis(filePath) {
const normalizedFilePath = this.normalizeFilePath(filePath);
return this.analyzeFile(normalizedFilePath);
}
/**
* Creates a unique key for component identification
*/
createComponentKey(filePath) {
return this.pathResolver.normalizeFilePath(filePath);
}
/**
* Creates a unique key for conditional identification
*/
createConditionalKey(filePath, condition, line, column) {
return `${this.createComponentKey(filePath)}::${condition}::${line}:${column}`;
}
/**
* Analyzes a single file using existing parsed content from ScanResult
*/
analyzeFile(filePath) {
try {
// Use existing file content from ScanResult - no file I/O
const content = this.scanResult.fileContents.get(filePath);
if (!content) {
console.warn(`File content not found in scanResult: ${filePath}`);
// Debug: Log some available files for comparison
const availableFiles = Array.from(this.scanResult.fileContents.keys()).slice(0, 3);
console.warn(`Available files sample:`, availableFiles);
return null;
}
// Parse AST only if not available (fallback)
const ast = (0, utils_1.parseFileToAST)(content);
if (!ast) {
return null;
}
const componentName = this.extractComponentName(filePath, ast);
// Pass current configuration to JSX analyzer
this.jsxAnalyzer.updateConfig(this.config);
const jsxReturns = this.jsxAnalyzer.analyzeAST(ast, content);
const imports = this.extractImports(ast, content);
const hasMultipleReturns = (0, utils_1.checkMultipleReturns)(ast);
return {
filePath,
componentName,
jsxReturns,
hasMultipleReturns,
imports,
};
}
catch (error) {
console.warn(`Error analyzing file ${filePath}:`, error);
return null;
}
}
/**
* Builds a ComponentFlowNode from file analysis using optimized services
*/
buildComponentFlowNode(fileAnalysis, depth) {
// Build conditional renders with optimized deduplication
const conditionalRenders = this.buildConditionalRenders(fileAnalysis, depth);
// Get unique child components using optimized resolution
const childComponents = this.extractChildComponents(fileAnalysis, depth);
return {
componentName: fileAnalysis.componentName,
filePath: fileAnalysis.filePath,
isExternal: false,
conditionalRenders,
children: childComponents,
};
}
/**
* Builds conditional render objects with optimized deduplication
*/
buildConditionalRenders(fileAnalysis, depth) {
const conditionalRenders = [];
for (const jsxReturn of fileAnalysis.jsxReturns) {
if (jsxReturn.hasConditional) {
for (const pattern of jsxReturn.conditionalPatterns) {
// Create unique conditional ID
const conditionalId = this.createConditionalKey(fileAnalysis.filePath, pattern.condition, pattern.position.line, pattern.position.column);
// Skip if this exact conditional has been processed
if (this.conditionalIds.has(conditionalId)) {
continue;
}
// Mark as processed
this.conditionalIds.add(conditionalId);
const trueBranch = this.resolveComponentReferences(pattern.trueBranch, fileAnalysis.filePath, depth + 1);
const falseBranch = pattern.falseBranch
? this.resolveComponentReferences(pattern.falseBranch, fileAnalysis.filePath, depth + 1)
: undefined;
// Build the conditional render with HTML elements
const conditionalRender = {
conditionType: pattern.type,
condition: pattern.condition,
trueBranch,
falseBranch,
position: pattern.position,
};
// Add HTML elements if available and enabled
if (this.config.includeHtmlElements) {
if (pattern.htmlElementsTrue &&
pattern.htmlElementsTrue.length > 0) {
conditionalRender.htmlElementsTrue = pattern.htmlElementsTrue;
}
if (pattern.htmlElementsFalse &&
pattern.htmlElementsFalse.length > 0) {
conditionalRender.htmlElementsFalse = pattern.htmlElementsFalse;
}
}
conditionalRenders.push(conditionalRender);
}
}
}
return conditionalRenders;
}
/**
* Extracts child components with optimized deduplication
*/
extractChildComponents(fileAnalysis, depth) {
const childComponents = [];
const seenComponentKeys = new Set();
// Process all JSX returns
for (const jsxReturn of fileAnalysis.jsxReturns) {
// Get components from direct references
const directComponents = this.resolveComponentReferences(jsxReturn.componentReferences, fileAnalysis.filePath, depth + 1);
this.addUniqueComponents(directComponents, childComponents, seenComponentKeys);
// Get components from conditional patterns
for (const pattern of jsxReturn.conditionalPatterns) {
// True branch components
const trueComponents = this.resolveComponentReferences(pattern.trueBranch, fileAnalysis.filePath, depth + 1);
this.addUniqueComponents(trueComponents, childComponents, seenComponentKeys);
// False branch components
if (pattern.falseBranch) {
const falseComponents = this.resolveComponentReferences(pattern.falseBranch, fileAnalysis.filePath, depth + 1);
this.addUniqueComponents(falseComponents, childComponents, seenComponentKeys);
}
}
}
return childComponents;
}
/**
* Adds components to list if not already seen
*/
addUniqueComponents(components, targetList, seenKeys) {
for (const component of components) {
const componentKey = this.createComponentKey(component.filePath);
if (!seenKeys.has(componentKey)) {
seenKeys.add(componentKey);
targetList.push(component);
}
}
}
/**
* Resolves component references to ComponentFlowNodes using optimized services
*/
resolveComponentReferences(references, currentFilePath, depth) {
const resolvedNodes = [];
const seenInThisResolution = new Set();
for (const reference of references) {
const resolved = this.resolveComponentReference(reference, currentFilePath);
if (resolved) {
const nodeKey = this.createComponentKey(resolved.filePath || resolved.componentName);
// Skip if already resolved in this batch
if (seenInThisResolution.has(nodeKey)) {
continue;
}
seenInThisResolution.add(nodeKey);
if (resolved.isExternal) {
// External component - add as-is
resolvedNodes.push(resolved);
}
else if (resolved.filePath) {
// Internal component - recursively analyze with depth limit
if (depth < this.maxDepth) {
const childNode = this.scanComponentFlow(resolved.filePath, depth);
if (childNode) {
// Use the resolved component name
childNode.componentName = reference.name;
resolvedNodes.push(childNode);
}
}
else {
// At max depth, add without children
resolvedNodes.push({
...resolved,
componentName: reference.name,
children: [],
conditionalRenders: [],
});
}
}
}
}
return resolvedNodes;
}
/**
* Resolves a component reference using optimized lookup services
*/
resolveComponentReference(reference, currentFilePath) {
const componentName = reference.name;
// Skip native HTML elements
if (this.isNativeHTMLElement(componentName)) {
return null;
}
// Skip React built-ins
if (this.isReactBuiltIn(componentName)) {
return null;
}
// Check if it's an external component using PathResolver
if (this.isExternallyImported(componentName, currentFilePath)) {
return {
componentName,
filePath: "",
isExternal: true,
conditionalRenders: [],
children: [],
};
}
// Try to resolve internal component using ComponentLookupService
const components = this.lookupService.getComponentsByName(componentName);
// Find the best match (prefer components in similar directory structure)
let bestMatch = null;
const currentDir = this.pathResolver.extractDirectory(currentFilePath);
for (const component of components) {
if (!bestMatch) {
bestMatch = component;
}
else {
// Prefer components in the same or parent directories
const componentDir = component.directory;
const bestMatchDir = bestMatch.directory;
if (componentDir === currentDir) {
bestMatch = component;
break;
}
else if (componentDir.includes(currentDir) &&
!bestMatchDir.includes(currentDir)) {
bestMatch = component;
}
}
}
if (bestMatch) {
return {
componentName,
filePath: bestMatch.fullPath,
isExternal: false,
conditionalRenders: [],
children: [],
};
}
// Component not found
return null;
}
/**
* Checks if a component name is externally imported using PathResolver
*/
isExternallyImported(componentName, filePath) {
const fileContent = this.scanResult.fileContents.get(filePath);
if (!fileContent) {
return false;
}
// Extract import statements and check if componentName is imported externally
const importRegex = /import\s+(?:{[^}]*\b(\w+)\b[^}]*}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
let match;
while ((match = importRegex.exec(fileContent)) !== null) {
const namedImport = match[1];
const defaultImport = match[2];
const importPath = match[3];
if (namedImport === componentName || defaultImport === componentName) {
// Use PathResolver to check if this import is external
if (this.pathResolver.isExternalPackage(importPath)) {
return true;
}
}
}
return false;
}
/**
* Checks if a component reference is a native HTML element
*/
isNativeHTMLElement(componentName) {
const htmlElements = new Set([
"div",
"span",
"p",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"a",
"img",
"button",
"input",
"form",
"label",
"select",
"option",
"textarea",
"table",
"thead",
"tbody",
"tr",
"td",
"th",
"ul",
"ol",
"li",
"nav",
"header",
"footer",
"main",
"section",
"article",
"aside",
"figure",
"figcaption",
"video",
"audio",
"canvas",
"svg",
"path",
"circle",
"rect",
"line",
"polygon",
"iframe",
"embed",
"object",
"pre",
"code",
"blockquote",
"hr",
"br",
"strong",
"em",
"small",
"mark",
"del",
"ins",
"sub",
"sup",
]);
return htmlElements.has(componentName.toLowerCase());
}
/**
* Checks if a component reference is a React built-in
*/
isReactBuiltIn(componentName) {
const reactBuiltIns = new Set([
"Fragment",
"Suspense",
"StrictMode",
"Profiler",
"React.Fragment",
"React.Suspense",
"React.StrictMode",
"React.Profiler",
"Transition",
"SuspenseList",
"ConcurrentMode",
"unstable_ConcurrentMode",
]);
return reactBuiltIns.has(componentName);
}
/**
* Extracts component name from file path or AST
*/
extractComponentName(filePath, ast) {
// First try to extract from default export
let componentName = (0, utils_1.extractDefaultExportName)(ast);
if (!componentName) {
// Use lookup service to get component name for this file
const component = this.lookupService.getComponentByPath(filePath);
if (component) {
componentName = component.name;
}
}
if (!componentName) {
// Fallback to filename
const fileName = filePath.split("/").pop() || "";
componentName = fileName.replace(/\.(tsx?|jsx?)$/, "");
}
return componentName;
}
/**
* Extracts import statements from AST
*/
extractImports(ast, content) {
const imports = [];
(0, traverse_1.default)(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
for (const specifier of path.node.specifiers) {
if (t.isImportDefaultSpecifier(specifier)) {
imports.push({
name: specifier.local.name,
source,
isDefault: true,
isNamespace: false,
localName: specifier.local.name,
});
}
else if (t.isImportNamespaceSpecifier(specifier)) {
imports.push({
name: "*",
source,
isDefault: false,
isNamespace: true,
localName: specifier.local.name,
});
}
else if (t.isImportSpecifier(specifier)) {
const importedName = t.isIdentifier(specifier.imported)
? specifier.imported.name
: specifier.imported.value;
imports.push({
name: importedName,
source,
isDefault: false,
isNamespace: false,
localName: specifier.local.name,
});
}
}
},
});
return imports;
}
/**
* Resets the analyzer state
*/
reset() {
this.analyzedComponents.clear();
this.conditionalIds.clear();
this.analysisInProgress.clear();
}
/**
* Gets summary statistics
*/
getSummaryStats(rootComponents) {
const allComponents = Array.from(this.analyzedComponents.values());
const totalConditionals = this.conditionalIds.size;
const totalComponents = allComponents.length;
const uniqueComponents = new Set(allComponents.map((comp) => this.createComponentKey(comp.filePath))).size;
// Count HTML elements in conditionals
let totalHtmlElementsInConditionals = 0;
if (this.config.includeHtmlElements) {
for (const component of allComponents) {
for (const conditional of component.conditionalRenders) {
if (conditional.htmlElementsTrue) {
totalHtmlElementsInConditionals +=
conditional.htmlElementsTrue.length;
}
if (conditional.htmlElementsFalse) {
totalHtmlElementsInConditionals +=
conditional.htmlElementsFalse.length;
}
}
}
}
return {
totalConditionals,
totalComponents,
uniqueComponents,
htmlElementsEnabled: this.config.includeHtmlElements,
totalHtmlElementsInConditionals,
};
}
/**
* Gets all analyzed components
*/
getAllAnalyzedComponents() {
return Array.from(this.analyzedComponents.values());
}
/**
* Gets conditional count
*/
getConditionalCount() {
return this.conditionalIds.size;
}
/**
* Updates configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
this.maxDepth = this.config.maxDepth;
// Reinitialize analyzers with new config
this.jsxAnalyzer.updateConfig(this.config);
// Clear caches if configuration changed significantly
if (config.includeHtmlElements !== undefined || config.htmlElementFilter) {
this.reset();
}
}
/**
* Gets current configuration
*/
getConfig() {
return { ...this.config };
}
}
exports.ComponentFlowScanner = ComponentFlowScanner;