sicua
Version:
A tool for analyzing project structure and dependencies
730 lines (729 loc) • 27.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FunctionExtractor = void 0;
const typescript_1 = __importDefault(require("typescript"));
const contextUtils_1 = require("../utils/contextUtils");
const reactSpecific_1 = require("../../../utils/ast/reactSpecific");
class FunctionExtractor {
constructor(sourceFile, content) {
this.sourceFile = sourceFile;
this.content = content;
}
/**
* Extracts all function context from the source file
*/
extractFunctionContext() {
const functions = [];
const callGraph = [];
// Extract function definitions
this.visitNode(this.sourceFile, (node) => {
if (this.isFunctionNode(node)) {
functions.push(this.extractFunction(node));
}
});
// Extract function calls
this.extractFunctionCalls(functions, callGraph);
const complexity = this.calculateComplexityMetrics(functions);
const patterns = this.analyzePatterns(functions);
return {
functions,
callGraph,
complexity,
patterns,
};
}
/**
* Extracts a single function definition
*/
extractFunction(node) {
const name = this.getFunctionName(node);
const kind = this.getFunctionKind(node);
const signature = this.extractSignature(node);
const complexity = this.calculateFunctionComplexity(node);
const patterns = this.identifyUsagePatterns(node);
const reactSpecific = this.extractReactInfo(node);
const async = this.isAsyncFunction(node);
const pure = this.isPureFunction(node);
const sideEffects = this.extractSideEffects(node);
const dependencies = this.extractFunctionDependencies(node);
const isExported = this.hasExportModifier(node);
const location = this.getLocation(node);
return {
name,
kind,
signature,
complexity,
patterns,
reactSpecific,
async,
pure,
sideEffects,
dependencies,
isExported,
exportType: isExported ? (0, contextUtils_1.getExportType)(node) : undefined,
description: this.extractJSDocComment(node),
location,
};
}
/**
* Extracts function signature
*/
extractSignature(node) {
let parameters = [];
let returnType = "void";
let generics = [];
let overloads = [];
if (typescript_1.default.isFunctionDeclaration(node) ||
typescript_1.default.isFunctionExpression(node) ||
typescript_1.default.isArrowFunction(node)) {
parameters = this.extractParameters(node.parameters);
returnType = node.type?.getText() || "void";
generics = this.extractGenerics(node.typeParameters);
}
else if (typescript_1.default.isMethodDeclaration(node)) {
parameters = this.extractParameters(node.parameters);
returnType = node.type?.getText() || "void";
generics = this.extractGenerics(node.typeParameters);
}
return {
parameters,
returnType,
generics,
overloads,
};
}
/**
* Extracts function parameters
*/
extractParameters(parameters) {
return parameters.map((param) => ({
name: param.name.getText(),
type: param.type?.getText() || "any",
optional: !!param.questionToken,
defaultValue: param.initializer?.getText(),
destructured: typescript_1.default.isObjectBindingPattern(param.name) ||
typescript_1.default.isArrayBindingPattern(param.name),
restParameter: !!param.dotDotDotToken,
}));
}
/**
* Calculates function complexity
*/
calculateFunctionComplexity(node) {
const cyclomaticComplexity = this.calculateCyclomaticComplexity(node);
const cognitiveComplexity = this.calculateCognitiveComplexity(node);
const linesOfCode = this.calculateLinesOfCode(node);
const nestingDepth = this.calculateNestingDepth(node);
let level = "low";
const totalScore = cyclomaticComplexity + cognitiveComplexity + nestingDepth * 2;
if (totalScore > 20)
level = "very-high";
else if (totalScore > 15)
level = "high";
else if (totalScore > 10)
level = "medium";
return {
cyclomaticComplexity,
cognitiveComplexity,
linesOfCode,
nestingDepth,
level,
};
}
/**
* Calculates cyclomatic complexity
*/
calculateCyclomaticComplexity(node) {
let complexity = 1; // Base complexity
this.visitNode(node, (child) => {
switch (child.kind) {
case typescript_1.default.SyntaxKind.IfStatement:
case typescript_1.default.SyntaxKind.WhileStatement:
case typescript_1.default.SyntaxKind.DoStatement:
case typescript_1.default.SyntaxKind.ForStatement:
case typescript_1.default.SyntaxKind.ForInStatement:
case typescript_1.default.SyntaxKind.ForOfStatement:
case typescript_1.default.SyntaxKind.CaseClause:
case typescript_1.default.SyntaxKind.CatchClause:
case typescript_1.default.SyntaxKind.ConditionalExpression:
complexity++;
break;
case typescript_1.default.SyntaxKind.BinaryExpression:
const binExpr = child;
if (binExpr.operatorToken.kind ===
typescript_1.default.SyntaxKind.AmpersandAmpersandToken ||
binExpr.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) {
complexity++;
}
break;
}
});
return complexity;
}
/**
* Calculates cognitive complexity
*/
calculateCognitiveComplexity(node) {
let complexity = 0;
let nestingLevel = 0;
const visit = (node, level) => {
switch (node.kind) {
case typescript_1.default.SyntaxKind.IfStatement:
case typescript_1.default.SyntaxKind.WhileStatement:
case typescript_1.default.SyntaxKind.DoStatement:
case typescript_1.default.SyntaxKind.ForStatement:
case typescript_1.default.SyntaxKind.ForInStatement:
case typescript_1.default.SyntaxKind.ForOfStatement:
case typescript_1.default.SyntaxKind.SwitchStatement:
complexity += 1 + level;
nestingLevel = Math.max(nestingLevel, level + 1);
typescript_1.default.forEachChild(node, (child) => visit(child, level + 1));
return;
case typescript_1.default.SyntaxKind.CatchClause:
case typescript_1.default.SyntaxKind.ConditionalExpression:
complexity += 1 + level;
break;
case typescript_1.default.SyntaxKind.BinaryExpression:
const binExpr = node;
if (binExpr.operatorToken.kind ===
typescript_1.default.SyntaxKind.AmpersandAmpersandToken ||
binExpr.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) {
complexity += 1;
}
break;
}
typescript_1.default.forEachChild(node, (child) => visit(child, level));
};
visit(node, 0);
return complexity;
}
/**
* Calculates lines of code for a function
*/
calculateLinesOfCode(node) {
const start = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
const end = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd());
return end.line - start.line + 1;
}
/**
* Calculates maximum nesting depth
*/
calculateNestingDepth(node) {
let maxDepth = 0;
const visit = (node, depth) => {
maxDepth = Math.max(maxDepth, depth);
const isNestingNode = typescript_1.default.isIfStatement(node) ||
typescript_1.default.isWhileStatement(node) ||
typescript_1.default.isForStatement(node) ||
typescript_1.default.isForInStatement(node) ||
typescript_1.default.isForOfStatement(node) ||
typescript_1.default.isSwitchStatement(node) ||
typescript_1.default.isTryStatement(node);
const nextDepth = isNestingNode ? depth + 1 : depth;
typescript_1.default.forEachChild(node, (child) => visit(child, nextDepth));
};
visit(node, 0);
return maxDepth;
}
/**
* Identifies usage patterns for a function
*/
identifyUsagePatterns(node) {
const patterns = [];
const nodeText = node.getText();
// Data transformation patterns
if (nodeText.includes(".map(") ||
nodeText.includes(".filter(") ||
nodeText.includes(".reduce(") ||
nodeText.includes(".sort(")) {
patterns.push("data-transformation");
}
// Event handling patterns
if (nodeText.includes("addEventListener") ||
nodeText.includes("onClick") ||
nodeText.includes("onChange") ||
nodeText.includes("onSubmit")) {
patterns.push("event-handling");
}
// State management patterns
if (nodeText.includes("useState") ||
nodeText.includes("useReducer") ||
nodeText.includes("setState") ||
nodeText.includes("dispatch")) {
patterns.push("state-management");
}
// API integration patterns
if (nodeText.includes("fetch(") ||
nodeText.includes("axios") ||
nodeText.includes("api.") ||
nodeText.includes("await ")) {
patterns.push("api-integration");
}
// UI composition patterns
if (nodeText.includes("return (") && nodeText.includes("<")) {
patterns.push("ui-composition");
}
// Validation patterns
if (nodeText.includes("validate") ||
nodeText.includes("schema") ||
nodeText.includes("yup.") ||
nodeText.includes("joi.")) {
patterns.push("validation");
}
// Error handling patterns
if (nodeText.includes("try") ||
nodeText.includes("catch") ||
nodeText.includes("throw") ||
nodeText.includes("Error(")) {
patterns.push("error-handling");
}
// Performance optimization patterns
if (nodeText.includes("useMemo") ||
nodeText.includes("useCallback") ||
nodeText.includes("React.memo") ||
nodeText.includes("lazy(")) {
patterns.push("performance-optimization");
}
// Business logic patterns
if (patterns.length === 0 && !nodeText.includes("<")) {
patterns.push("business-logic");
}
// Utility function patterns
if (nodeText.length < 100 && !patterns.includes("ui-composition")) {
patterns.push("utility-function");
}
return patterns;
}
/**
* Extracts React-specific information
*/
extractReactInfo(node) {
const nodeText = node.getText();
// Check if it's a React component or hook
const isComponent = (0, reactSpecific_1.isReactComponent)(node);
const isHook = this.isReactHook(node);
if (!isComponent && !isHook) {
return undefined;
}
const hooksUsed = this.extractHooksUsed(nodeText);
const jsxComplexity = this.calculateJSXComplexity(nodeText);
const propTypes = this.extractPropTypes(node);
const stateUsage = this.extractStateUsage(nodeText);
const effectUsage = this.extractEffectUsage(nodeText);
return {
isComponent,
isHook,
hooksUsed,
jsxComplexity,
propTypes,
stateUsage,
effectUsage,
};
}
/**
* Checks if function is a React hook
*/
isReactHook(node) {
const name = this.getFunctionName(node);
return name
? name.startsWith("use") && name[3]?.toUpperCase() === name[3]
: false;
}
/**
* Extracts hooks used in the function
*/
extractHooksUsed(nodeText) {
const hookPattern = /\b(use[A-Z]\w*)\s*\(/g;
const hooks = [];
let match;
while ((match = hookPattern.exec(nodeText)) !== null) {
if (!hooks.includes(match[1])) {
hooks.push(match[1]);
}
}
return hooks;
}
/**
* Calculates JSX complexity
*/
calculateJSXComplexity(nodeText) {
const jsxElements = (nodeText.match(/<[A-Z]/g) || []).length;
const conditionalRendering = (nodeText.match(/\{[^}]*\?[^}]*:/g) || [])
.length;
const mapOperations = (nodeText.match(/\.map\(/g) || []).length;
return jsxElements + conditionalRendering * 2 + mapOperations * 2;
}
/**
* Extracts prop types from function parameters
*/
extractPropTypes(node) {
if (!typescript_1.default.isFunctionDeclaration(node) &&
!typescript_1.default.isFunctionExpression(node) &&
!typescript_1.default.isArrowFunction(node)) {
return undefined;
}
if (node.parameters.length === 0)
return [];
const firstParam = node.parameters[0];
if (firstParam.type) {
const typeText = firstParam.type.getText();
// Extract property names from interface/type
const propPattern = /(\w+)\s*[?:]?\s*:/g;
const props = [];
let match;
while ((match = propPattern.exec(typeText)) !== null) {
props.push(match[1]);
}
return props;
}
return undefined;
}
/**
* Extracts state usage information
*/
extractStateUsage(nodeText) {
const hasState = nodeText.includes("useState") ||
nodeText.includes("useReducer") ||
nodeText.includes("useContext");
const stateVariables = [];
const useStatePattern = /const\s*\[([^,\]]+),/g;
let match;
while ((match = useStatePattern.exec(nodeText)) !== null) {
stateVariables.push(match[1].trim());
}
const reducers = [];
const useReducerPattern = /const\s*\[([^,\]]+),\s*([^,\]]+)\]\s*=\s*useReducer/g;
while ((match = useReducerPattern.exec(nodeText)) !== null) {
reducers.push(match[1].trim());
}
const contextUsage = [];
const useContextPattern = /const\s+(\w+)\s*=\s*useContext\(([^)]+)\)/g;
while ((match = useContextPattern.exec(nodeText)) !== null) {
contextUsage.push(match[2].trim());
}
return {
hasState,
stateVariables,
reducers,
contextUsage,
};
}
/**
* Extracts effect usage information
*/
extractEffectUsage(nodeText) {
const effects = [];
const dependencies = [];
// Extract useEffect calls
const effectPattern = /(useEffect|useLayoutEffect|useMemo|useCallback)\s*\(\s*[^,]+,\s*\[([^\]]*)\]/g;
let match;
while ((match = effectPattern.exec(nodeText)) !== null) {
const type = match[1];
const depsString = match[2];
const deps = depsString
? depsString
.split(",")
.map((d) => d.trim())
.filter((d) => d)
: [];
// Check for cleanup function
const effectStart = match.index;
const effectBlock = this.extractEffectBlock(nodeText, effectStart);
const hasCleanup = (effectBlock.includes("return ") && effectBlock.includes("=>")) ||
effectBlock.includes("return function");
effects.push({
type,
dependencies: deps,
hasCleanup,
});
dependencies.push(deps);
}
const cleanupFunctions = effects.filter((e) => e.hasCleanup).length;
return {
effects,
cleanupFunctions,
dependencies,
};
}
/**
* Extracts the effect block content
*/
extractEffectBlock(nodeText, startIndex) {
let braceCount = 0;
let inEffect = false;
let effectContent = "";
for (let i = startIndex; i < nodeText.length; i++) {
const char = nodeText[i];
if (char === "(" && !inEffect) {
inEffect = true;
continue;
}
if (inEffect) {
if (char === "{")
braceCount++;
if (char === "}")
braceCount--;
effectContent += char;
if (braceCount === 0 && char === "}") {
break;
}
}
}
return effectContent;
}
/**
* Extracts side effects from function
*/
extractSideEffects(node) {
const sideEffects = [];
const nodeText = node.getText();
// API calls
if (nodeText.includes("fetch(") ||
nodeText.includes(".get(") ||
nodeText.includes(".post(")) {
sideEffects.push({
type: "api-call",
description: "Makes HTTP requests",
location: this.getLocation(node),
});
}
// DOM manipulation
if (nodeText.includes("document.") ||
nodeText.includes("window.") ||
nodeText.includes(".getElementById")) {
sideEffects.push({
type: "dom-manipulation",
description: "Directly manipulates DOM",
location: this.getLocation(node),
});
}
// Storage access
if (nodeText.includes("localStorage") ||
nodeText.includes("sessionStorage") ||
nodeText.includes("indexedDB")) {
sideEffects.push({
type: "storage",
description: "Accesses browser storage",
location: this.getLocation(node),
});
}
// Console operations
if (nodeText.includes("console.")) {
sideEffects.push({
type: "console",
description: "Logs to console",
location: this.getLocation(node),
});
}
return sideEffects;
}
/**
* Extracts function dependencies
*/
extractFunctionDependencies(node) {
const dependencies = [];
const nodeText = node.getText();
// Extract function calls
const functionCallPattern = /(\w+)\s*\(/g;
let match;
const builtIns = [
"console",
"setTimeout",
"setInterval",
"parseInt",
"parseFloat",
"JSON",
];
while ((match = functionCallPattern.exec(nodeText)) !== null) {
const funcName = match[1];
if (!builtIns.includes(funcName) &&
funcName !== this.getFunctionName(node)) {
dependencies.push({
name: funcName,
type: "function-call",
});
}
}
return dependencies;
}
/**
* Extracts function calls for call graph
*/
extractFunctionCalls(functions, callGraph) {
functions.forEach((func) => {
func.dependencies.forEach((dep) => {
if (dep.type === "function-call") {
callGraph.push({
caller: func.name,
callee: dep.name,
location: func.location,
isConditional: this.isConditionalCall(func, dep.name),
isInLoop: this.isInLoop(func, dep.name),
});
}
});
});
}
/**
* Calculates overall complexity metrics
*/
calculateComplexityMetrics(functions) {
const totalFunctions = functions.length;
const complexities = functions.map((f) => f.complexity.cyclomaticComplexity);
const averageComplexity = complexities.length > 0
? complexities.reduce((a, b) => a + b, 0) / complexities.length
: 0;
const highComplexityCount = functions.filter((f) => f.complexity.level === "high" || f.complexity.level === "very-high").length;
const maxNestingDepth = Math.max(...functions.map((f) => f.complexity.nestingDepth), 0);
const totalLinesOfCode = functions.reduce((total, f) => total + f.complexity.linesOfCode, 0);
return {
totalFunctions,
averageComplexity,
highComplexityCount,
maxNestingDepth,
totalLinesOfCode,
};
}
/**
* Analyzes patterns across all functions
*/
analyzePatterns(functions) {
const allPatterns = functions.flatMap((f) => f.patterns);
const reactFunctions = functions.filter((f) => f.reactSpecific);
const functionalPatterns = [
...new Set(allPatterns.filter((p) => [
"data-transformation",
"utility-function",
"business-logic",
].includes(p))),
];
const reactPatterns = [
...new Set(reactFunctions.flatMap((f) => (0, contextUtils_1.identifyReactPatterns)(this.sourceFile, this.content))),
];
const asyncPatterns = functions.filter((f) => f.async).length > 0 ? ["async-await"] : [];
const errorHandlingPatterns = allPatterns.includes("error-handling")
? ["try-catch"]
: [];
return {
functionalPatterns,
reactPatterns,
asyncPatterns,
errorHandlingPatterns,
};
}
/**
* Helper methods
*/
isFunctionNode(node) {
return (typescript_1.default.isFunctionDeclaration(node) ||
typescript_1.default.isFunctionExpression(node) ||
typescript_1.default.isArrowFunction(node) ||
typescript_1.default.isMethodDeclaration(node) ||
typescript_1.default.isConstructorDeclaration(node) ||
typescript_1.default.isGetAccessorDeclaration(node) ||
typescript_1.default.isSetAccessorDeclaration(node));
}
getFunctionName(node) {
if (typescript_1.default.isFunctionDeclaration(node)) {
return node.name?.text || "anonymous";
}
if (typescript_1.default.isMethodDeclaration(node) ||
typescript_1.default.isGetAccessorDeclaration(node) ||
typescript_1.default.isSetAccessorDeclaration(node)) {
return node.name?.getText() || "anonymous";
}
if (typescript_1.default.isConstructorDeclaration(node)) {
return "constructor";
}
return "anonymous";
}
getFunctionKind(node) {
if (typescript_1.default.isFunctionDeclaration(node))
return "function-declaration";
if (typescript_1.default.isFunctionExpression(node))
return "function-expression";
if (typescript_1.default.isArrowFunction(node))
return "arrow-function";
if (typescript_1.default.isMethodDeclaration(node))
return "method";
if (typescript_1.default.isConstructorDeclaration(node))
return "constructor";
if (typescript_1.default.isGetAccessorDeclaration(node))
return "getter";
if (typescript_1.default.isSetAccessorDeclaration(node))
return "setter";
return "function-declaration";
}
extractGenerics(typeParameters) {
if (!typeParameters)
return [];
return typeParameters.map((tp) => tp.name.text);
}
isAsyncFunction(node) {
if (typescript_1.default.isFunctionDeclaration(node) ||
typescript_1.default.isFunctionExpression(node) ||
typescript_1.default.isArrowFunction(node) ||
typescript_1.default.isMethodDeclaration(node)) {
return !!node.modifiers?.some((mod) => mod.kind === typescript_1.default.SyntaxKind.AsyncKeyword);
}
return false;
}
isPureFunction(node) {
// Simple heuristic: no side effects detected
return this.extractSideEffects(node).length === 0;
}
hasExportModifier(node) {
return typescript_1.default.getCombinedModifierFlags(node) & typescript_1.default.ModifierFlags.Export
? true
: false;
}
getLocation(node) {
const start = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
const end = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd());
return {
startLine: start.line + 1,
endLine: end.line + 1,
startColumn: start.character,
endColumn: end.character,
};
}
extractJSDocComment(node) {
const sourceFile = node.getSourceFile();
const fullText = sourceFile.getFullText();
const commentRanges = typescript_1.default.getLeadingCommentRanges(fullText, node.getFullStart());
if (commentRanges && commentRanges.length > 0) {
const lastComment = commentRanges[commentRanges.length - 1];
const commentText = fullText.substring(lastComment.pos, lastComment.end);
if (commentText.startsWith("/**")) {
return commentText
.replace(/\/\*\*|\*\/|\* ?/g, "")
.trim()
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join(" ");
}
}
return undefined;
}
visitNode(node, callback) {
callback(node);
typescript_1.default.forEachChild(node, (child) => this.visitNode(child, callback));
}
isConditionalCall(func, callee) {
// Simple heuristic: check if function contains conditional statements
return func.complexity.cyclomaticComplexity > 1;
}
isInLoop(func, callee) {
// Simple heuristic: check if function contains loops
const nodeText = this.content.substring(this.sourceFile.getPositionOfLineAndCharacter(func.location.startLine - 1, 0), this.sourceFile.getPositionOfLineAndCharacter(func.location.endLine - 1, func.location.endColumn));
return (nodeText.includes("for(") ||
nodeText.includes("while(") ||
nodeText.includes(".map(") ||
nodeText.includes(".forEach("));
}
}
exports.FunctionExtractor = FunctionExtractor;