sicua
Version:
A tool for analyzing project structure and dependencies
518 lines (517 loc) • 22.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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConditionalParser = void 0;
const t = __importStar(require("@babel/types"));
const types_1 = require("../types");
const utils_1 = require("../utils");
const reactSpecific_1 = require("../../../utils/ast/reactSpecific");
/**
* Core parser for detecting and analyzing conditional rendering patterns in JSX
*/
class ConditionalParser {
constructor(config) {
// Use provided config or create default
this.config = config || {
maxDepth: 10,
includeExternalComponents: true,
excludePatterns: [],
onlyAnalyzeRoutes: [],
includeHtmlElements: false,
htmlElementFilter: types_1.DEFAULT_HTML_ELEMENT_FILTER,
};
}
/**
* Analyzes a JSX expression for conditional rendering patterns
*/
analyzeExpression(expression, sourceCode) {
const patterns = [];
if (t.isConditionalExpression(expression)) {
patterns.push(this.parseTernaryExpression(expression, sourceCode));
}
else if (t.isLogicalExpression(expression)) {
const logicalPattern = this.parseLogicalExpression(expression, sourceCode);
if (logicalPattern) {
patterns.push(logicalPattern);
}
}
else if (t.isCallExpression(expression)) {
// Handle function calls that might return conditional JSX
const callPatterns = this.analyzeCallExpression(expression, sourceCode);
patterns.push(...callPatterns);
}
else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) {
// Check if JSX element itself contains conditional expressions
const jsxPatterns = this.analyzeJSXExpressions(expression, sourceCode);
patterns.push(...jsxPatterns);
}
return patterns;
}
/**
* Analyzes JSX expressions for conditional patterns within JSX
*/
analyzeJSXExpressions(jsxNode, sourceCode) {
const patterns = [];
if (t.isJSXElement(jsxNode)) {
// Check children for JSX expression containers with conditionals
for (const child of jsxNode.children) {
if (t.isJSXExpressionContainer(child) &&
!t.isJSXEmptyExpression(child.expression)) {
const childPatterns = this.analyzeExpression(child.expression, sourceCode);
patterns.push(...childPatterns);
}
else if (t.isJSXElement(child) || t.isJSXFragment(child)) {
const nestedPatterns = this.analyzeJSXExpressions(child, sourceCode);
patterns.push(...nestedPatterns);
}
}
}
else if (t.isJSXFragment(jsxNode)) {
// Check fragment children
for (const child of jsxNode.children) {
if (t.isJSXExpressionContainer(child) &&
!t.isJSXEmptyExpression(child.expression)) {
const childPatterns = this.analyzeExpression(child.expression, sourceCode);
patterns.push(...childPatterns);
}
else if (t.isJSXElement(child) || t.isJSXFragment(child)) {
const nestedPatterns = this.analyzeJSXExpressions(child, sourceCode);
patterns.push(...nestedPatterns);
}
}
}
return patterns;
}
/**
* Analyzes function body for conditional return statements
*/
analyzeFunctionBody(body, sourceCode) {
const patterns = [];
if (t.isBlockStatement(body)) {
patterns.push(...this.analyzeBlockStatement(body, sourceCode));
}
else if (t.isExpression(body)) {
patterns.push(...this.analyzeExpression(body, sourceCode));
}
return patterns;
}
/**
* Parses ternary conditional expressions (condition ? true : false) - ENHANCED
*/
parseTernaryExpression(expression, sourceCode) {
const condition = this.extractConditionString(expression.test, sourceCode);
// Extract both components and HTML elements
const { components: trueBranchComponents, htmlElements: trueBranchHtml } = this.extractAllReferences(expression.consequent, sourceCode);
const { components: falseBranchComponents, htmlElements: falseBranchHtml } = this.extractAllReferences(expression.alternate, sourceCode);
return {
type: "ternary",
condition,
trueBranch: trueBranchComponents,
falseBranch: falseBranchComponents,
htmlElementsTrue: trueBranchHtml,
htmlElementsFalse: falseBranchHtml,
position: (0, utils_1.getCodePosition)(expression),
};
}
/**
* Parses logical AND expressions (condition && <Component />) - ENHANCED
*/
parseLogicalExpression(expression, sourceCode) {
if (expression.operator === "&&") {
const condition = this.extractConditionString(expression.left, sourceCode);
const { components: trueBranchComponents, htmlElements: trueBranchHtml } = this.extractAllReferences(expression.right, sourceCode);
// Only create pattern if we found components or HTML elements in the right side
if (trueBranchComponents.length > 0 || trueBranchHtml.length > 0) {
return {
type: "logical_and",
condition,
trueBranch: trueBranchComponents,
// Logical AND has no explicit false branch
falseBranch: undefined,
htmlElementsTrue: trueBranchHtml,
htmlElementsFalse: undefined,
position: (0, utils_1.getCodePosition)(expression),
};
}
}
else if (expression.operator === "||") {
// Handle OR expressions (fallback rendering)
const condition = this.extractConditionString(expression.left, sourceCode);
const { components: trueBranchComponents, htmlElements: trueBranchHtml } = this.extractAllReferences(expression.left, sourceCode);
const { components: falseBranchComponents, htmlElements: falseBranchHtml, } = this.extractAllReferences(expression.right, sourceCode);
if (trueBranchComponents.length > 0 ||
falseBranchComponents.length > 0 ||
trueBranchHtml.length > 0 ||
falseBranchHtml.length > 0) {
return {
type: "logical_and", // Treat OR as logical for now
condition,
trueBranch: trueBranchComponents,
falseBranch: falseBranchComponents,
htmlElementsTrue: trueBranchHtml,
htmlElementsFalse: falseBranchHtml,
position: (0, utils_1.getCodePosition)(expression),
};
}
}
return null;
}
/**
* Analyzes block statements for if/else and early return patterns
*/
analyzeBlockStatement(block, sourceCode) {
const patterns = [];
for (const statement of block.body) {
if (t.isIfStatement(statement)) {
patterns.push(this.parseIfStatement(statement, sourceCode));
}
else if (t.isReturnStatement(statement)) {
const returnPatterns = this.analyzeReturnStatement(statement, sourceCode);
patterns.push(...returnPatterns);
}
else if (t.isSwitchStatement(statement)) {
patterns.push(this.parseSwitchStatement(statement, sourceCode));
}
}
return patterns;
}
/**
* Parses if/else statements - ENHANCED
*/
parseIfStatement(statement, sourceCode) {
const condition = this.extractConditionString(statement.test, sourceCode);
const { components: trueBranchComponents, htmlElements: trueBranchHtml } = this.extractReferencesFromStatement(statement.consequent, sourceCode);
let falseBranchComponents = [];
let falseBranchHtml = [];
if (statement.alternate) {
const falseResult = this.extractReferencesFromStatement(statement.alternate, sourceCode);
falseBranchComponents = falseResult.components;
falseBranchHtml = falseResult.htmlElements;
}
return {
type: "if_statement",
condition,
trueBranch: trueBranchComponents,
falseBranch: falseBranchComponents.length > 0 ? falseBranchComponents : undefined,
htmlElementsTrue: trueBranchHtml,
htmlElementsFalse: falseBranchHtml.length > 0 ? falseBranchHtml : undefined,
position: (0, utils_1.getCodePosition)(statement),
};
}
/**
* Parses switch statements - ENHANCED
*/
parseSwitchStatement(statement, sourceCode) {
const condition = this.extractConditionString(statement.discriminant, sourceCode);
const trueBranchComponents = [];
const trueBranchHtml = [];
for (const caseClause of statement.cases) {
for (const stmt of caseClause.consequent) {
const { components, htmlElements } = this.extractReferencesFromStatement(stmt, sourceCode);
trueBranchComponents.push(...components);
trueBranchHtml.push(...htmlElements);
}
}
return {
type: "switch_statement",
condition,
trueBranch: trueBranchComponents,
htmlElementsTrue: trueBranchHtml,
position: (0, utils_1.getCodePosition)(statement),
};
}
/**
* Analyzes return statements for conditional patterns
*/
analyzeReturnStatement(statement, sourceCode) {
if (!statement.argument) {
return [];
}
// First check if the return statement itself contains conditional logic
const patterns = this.analyzeExpression(statement.argument, sourceCode);
// If no conditional patterns found but contains JSX, treat as early return
if (patterns.length === 0 && (0, reactSpecific_1.containsJSX)(statement.argument)) {
const { components, htmlElements } = this.extractAllReferences(statement.argument, sourceCode);
if (components.length > 0 || htmlElements.length > 0) {
patterns.push({
type: "early_return",
condition: "early return",
trueBranch: components,
htmlElementsTrue: htmlElements,
position: (0, utils_1.getCodePosition)(statement),
});
}
}
return patterns;
}
/**
* Analyzes call expressions that might return conditional JSX
*/
analyzeCallExpression(expression, sourceCode) {
// Handle render functions or factory functions
if (t.isIdentifier(expression.callee) &&
expression.callee.name.toLowerCase().includes("render")) {
// This might be a render function that returns conditional JSX
return [];
}
return [];
}
/**
* NEW: Extracts both component and HTML element references from various node types
*/
extractAllReferences(node, sourceCode) {
const components = [];
const htmlElements = [];
if (t.isJSXElement(node)) {
// Check if this is a component or HTML element
const elementName = (0, reactSpecific_1.getJSXElementName)(node.openingElement.name);
if ((0, reactSpecific_1.isReactComponentElement)(node)) {
// It's a React component
components.push((0, utils_1.parseJSXElement)(node, sourceCode));
}
else if (this.shouldIncludeHtmlElement(elementName)) {
// It's an HTML element and we should track it
htmlElements.push(this.parseHTMLElement(node, sourceCode));
}
// Also check children for more components/elements
for (const child of node.children) {
if (t.isJSXElement(child)) {
const childRefs = this.extractAllReferences(child, sourceCode);
components.push(...childRefs.components);
htmlElements.push(...childRefs.htmlElements);
}
else if (t.isJSXExpressionContainer(child) &&
!t.isJSXEmptyExpression(child.expression)) {
const exprRefs = this.extractAllReferences(child.expression, sourceCode);
components.push(...exprRefs.components);
htmlElements.push(...exprRefs.htmlElements);
}
}
}
else if (t.isJSXFragment(node)) {
// Handle React fragments
for (const child of node.children) {
if (t.isJSXElement(child)) {
const childRefs = this.extractAllReferences(child, sourceCode);
components.push(...childRefs.components);
htmlElements.push(...childRefs.htmlElements);
}
else if (t.isJSXExpressionContainer(child) &&
!t.isJSXEmptyExpression(child.expression)) {
const exprRefs = this.extractAllReferences(child.expression, sourceCode);
components.push(...exprRefs.components);
htmlElements.push(...exprRefs.htmlElements);
}
}
}
else if (t.isArrayExpression(node)) {
// Handle arrays of JSX elements (like map results)
for (const element of node.elements) {
if (element) {
const elementRefs = this.extractAllReferences(element, sourceCode);
components.push(...elementRefs.components);
htmlElements.push(...elementRefs.htmlElements);
}
}
}
else if (t.isCallExpression(node)) {
// Handle function calls that might return JSX
if (t.isIdentifier(node.callee)) {
components.push({
name: node.callee.name,
isJSXElement: false,
props: [],
position: (0, utils_1.getCodePosition)(node),
});
}
else if (t.isMemberExpression(node.callee)) {
// Handle member expressions like React.createElement
const memberName = this.extractMemberExpressionName(node.callee);
if (memberName) {
components.push({
name: memberName,
isJSXElement: false,
props: [],
position: (0, utils_1.getCodePosition)(node),
});
}
}
}
else if (t.isConditionalExpression(node)) {
// Handle nested conditionals - extract from both branches
const consequentRefs = this.extractAllReferences(node.consequent, sourceCode);
const alternateRefs = this.extractAllReferences(node.alternate, sourceCode);
components.push(...consequentRefs.components, ...alternateRefs.components);
htmlElements.push(...consequentRefs.htmlElements, ...alternateRefs.htmlElements);
}
else if (t.isLogicalExpression(node)) {
// Handle logical expressions
if (node.operator === "&&") {
const rightRefs = this.extractAllReferences(node.right, sourceCode);
components.push(...rightRefs.components);
htmlElements.push(...rightRefs.htmlElements);
}
else if (node.operator === "||") {
const leftRefs = this.extractAllReferences(node.left, sourceCode);
const rightRefs = this.extractAllReferences(node.right, sourceCode);
components.push(...leftRefs.components, ...rightRefs.components);
htmlElements.push(...leftRefs.htmlElements, ...rightRefs.htmlElements);
}
}
else if (t.isIdentifier(node)) {
// Handle bare identifiers that might be component references
// Only include if it's capitalized (component convention)
if (node.name.charAt(0) === node.name.charAt(0).toUpperCase()) {
components.push({
name: node.name,
isJSXElement: false,
props: [],
position: (0, utils_1.getCodePosition)(node),
});
}
}
else if (t.isMemberExpression(node)) {
// Handle member expressions like React.Component
const memberName = this.extractMemberExpressionName(node);
if (memberName &&
memberName.charAt(0) === memberName.charAt(0).toUpperCase()) {
components.push({
name: memberName,
isJSXElement: false,
props: [],
position: (0, utils_1.getCodePosition)(node),
});
}
}
return { components, htmlElements };
}
/**
* NEW: Extracts references from statements
*/
extractReferencesFromStatement(statement, sourceCode) {
if (t.isReturnStatement(statement) && statement.argument) {
return this.extractAllReferences(statement.argument, sourceCode);
}
else if (t.isExpressionStatement(statement)) {
return this.extractAllReferences(statement.expression, sourceCode);
}
else if (t.isBlockStatement(statement)) {
const components = [];
const htmlElements = [];
for (const stmt of statement.body) {
const refs = this.extractReferencesFromStatement(stmt, sourceCode);
components.push(...refs.components);
htmlElements.push(...refs.htmlElements);
}
return { components, htmlElements };
}
return { components: [], htmlElements: [] };
}
/**
* NEW: Parses HTML element into HTMLElementReference
*/
parseHTMLElement(element, sourceCode) {
const tagName = (0, reactSpecific_1.getJSXElementName)(element.openingElement.name);
const props = (0, utils_1.parseJSXProps)(element.openingElement.attributes, sourceCode);
const hasChildren = element.children.length > 0;
// Extract text content if enabled and element has text children
let textContent;
if (this.config.htmlElementFilter.captureTextContent && hasChildren) {
textContent = this.extractTextContent(element, sourceCode);
// Truncate if too long
if (textContent &&
textContent.length > this.config.htmlElementFilter.maxTextLength) {
textContent =
textContent.substring(0, this.config.htmlElementFilter.maxTextLength) + "...";
}
}
return {
tagName,
props,
hasChildren,
textContent,
position: (0, utils_1.getCodePosition)(element),
};
}
/**
* NEW: Extracts text content from JSX element
*/
extractTextContent(element, sourceCode) {
const textParts = [];
for (const child of element.children) {
if (t.isJSXText(child)) {
// Direct text content
textParts.push(child.value.trim());
}
else if (t.isJSXExpressionContainer(child) &&
t.isStringLiteral(child.expression)) {
// String literals in expressions
textParts.push(child.expression.value);
}
// Could extend to handle more complex text extraction
}
const fullText = textParts.join(" ").trim();
return fullText.length > 0 ? fullText : undefined;
}
/**
* NEW: Determines if an HTML element should be included based on configuration
*/
shouldIncludeHtmlElement(tagName) {
if (!this.config.includeHtmlElements) {
return false;
}
const filter = this.config.htmlElementFilter;
// Check exclude list first
if (filter.excludeTags.includes(tagName)) {
return false;
}
// If includeAll is true, include everything not excluded
if (filter.includeAll) {
return true;
}
// Otherwise, only include if in the include list
return filter.includeTags.includes(tagName);
}
/**
* Helper function to extract name from member expressions
*/
extractMemberExpressionName(memberExpr) {
if (t.isIdentifier(memberExpr.object) &&
t.isIdentifier(memberExpr.property)) {
return `${memberExpr.object.name}.${memberExpr.property.name}`;
}
else if (t.isIdentifier(memberExpr.property)) {
return memberExpr.property.name;
}
return null;
}
/**
* Extracts condition string from test expressions
*/
extractConditionString(test, sourceCode) {
return (0, utils_1.extractExpressionString)(test, sourceCode);
}
}
exports.ConditionalParser = ConditionalParser;