sicua
Version:
A tool for analyzing project structure and dependencies
490 lines (487 loc) • 19.9 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.ASTUtils = void 0;
const typescript_1 = __importDefault(require("typescript"));
const reactSpecific_1 = require("./reactSpecific");
class ASTUtils {
/**
* Gets the name of a function-like declaration
*/
/* static getFunctionName(node: ts.Node | ts.FunctionDeclaration | ts.ArrowFunction): string {} */
static getFunctionName(node) {
// Function declaration
if (typescript_1.default.isFunctionDeclaration(node) && node.name) {
return node.name.text;
}
// Variable declaration (includes arrow functions)
else if (typescript_1.default.isVariableDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
return node.name.text;
}
// Method declaration
else if (typescript_1.default.isMethodDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
return node.name.text;
}
// Arrow function cases
if (typescript_1.default.isArrowFunction(node)) {
let parent = node.parent;
// Handle variable declarations
if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
return parent.name.text;
}
// Handle property assignments
if (typescript_1.default.isPropertyAssignment(parent) && parent.name) {
if (typescript_1.default.isIdentifier(parent.name)) {
return parent.name.text;
}
if (typescript_1.default.isStringLiteral(parent.name)) {
return parent.name.text;
}
}
// Handle binary expressions (e.g., this.handler = () => {})
if (typescript_1.default.isBinaryExpression(parent) && parent.left) {
if (typescript_1.default.isPropertyAccessExpression(parent.left)) {
return parent.left.name.text;
}
if (typescript_1.default.isIdentifier(parent.left)) {
return parent.left.text;
}
}
// Handle export default arrow function
if (typescript_1.default.isExportAssignment(parent) && parent.expression === node) {
const sourceFile = node.getSourceFile();
const fileName = sourceFile.fileName;
const baseName = fileName.split("/").pop()?.split(".")[0];
return baseName || "DefaultExport";
}
// Handle object literal method shorthand
if (typescript_1.default.isShorthandPropertyAssignment(parent)) {
return parent.name.text;
}
}
// Function expression
else if (typescript_1.default.isFunctionExpression(node) && node.name) {
return node.name.text;
}
// Property assignment (e.g., exports.Component = ...)
else if (typescript_1.default.isPropertyAssignment(node) && typescript_1.default.isIdentifier(node.name)) {
return node.name.text;
}
// Binary expression (e.g., module.exports = ...)
else if (typescript_1.default.isBinaryExpression(node) &&
typescript_1.default.isPropertyAccessExpression(node.left)) {
return node.left.name.text;
}
// Named exports (e.g., export const MyComponent = ...)
else if (typescript_1.default.isExportDeclaration(node) &&
node.exportClause &&
typescript_1.default.isNamedExports(node.exportClause)) {
const elements = node.exportClause.elements;
if (elements.length === 1 && typescript_1.default.isExportSpecifier(elements[0])) {
return elements[0].name.text;
}
}
// Export assignment (e.g., export = MyComponent)
else if (typescript_1.default.isExportAssignment(node) && typescript_1.default.isIdentifier(node.expression)) {
return node.expression.text;
}
// Variable declaration within export declaration
else if (typescript_1.default.isVariableStatement(node)) {
const declarations = node.declarationList.declarations;
if (declarations.length === 1 && typescript_1.default.isIdentifier(declarations[0].name)) {
return declarations[0].name.text;
}
}
// Handle nested function declarations in namespace/module
else if (typescript_1.default.isModuleDeclaration(node)) {
const body = node.body;
if (body && typescript_1.default.isModuleBlock(body)) {
for (const statement of body.statements) {
const name = this.getFunctionName(statement);
if (name)
return name;
}
}
}
return "Anonymous Function";
}
/**
* Finds all nodes matching a specific predicate across all source files
*/
static findNodesInFiles(sourceFiles, predicate) {
const results = new Map();
sourceFiles.forEach((sourceFile, filePath) => {
const fileResults = [];
function visit(node) {
if (predicate(node)) {
fileResults.push(node);
}
typescript_1.default.forEachChild(node, visit);
}
visit(sourceFile);
if (fileResults.length > 0) {
results.set(filePath, fileResults);
}
});
return results;
}
/**
* Finds all nodes matching a predicate in a single file
*/
static findNodes(sourceFile, predicate) {
const results = [];
function visit(node) {
if (predicate(node)) {
results.push(node);
}
typescript_1.default.forEachChild(node, visit);
}
visit(sourceFile);
return results;
}
/**
* Gets the location info for a node with source file context
*/
static getNodeLocation(node, sourceFile) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
return { line, column: character };
}
/**
* Gets the source file for a node
*/
static getSourceFileForNode(node, sourceFiles) {
const fileName = node.getSourceFile()?.fileName;
return fileName ? sourceFiles.get(fileName) : undefined;
}
/**
* Builds a path to a node from its ancestors
*/
static getNodePath(node) {
const path = [];
let current = node;
while (current) {
path.unshift(current);
current = current.parent;
}
return path;
}
/**
* Gets the nearest parent matching a predicate
*/
static findNearestParent(node, predicate) {
let current = node.parent;
while (current) {
if (predicate(current)) {
return current;
}
current = current.parent;
}
return undefined;
}
/**
* Finds all references to an identifier across all files
*/
static findReferencesInFiles(sourceFiles, identifier) {
const results = new Map();
sourceFiles.forEach((sourceFile, filePath) => {
const fileResults = this.findReferences(sourceFile, identifier);
if (fileResults.length > 0) {
results.set(filePath, fileResults);
}
});
return results;
}
/**
* Finds all references to an identifier in a single file
*/
static findReferences(sourceFile, identifier) {
return this.findNodes(sourceFile, typescript_1.default.isIdentifier).filter((id) => id.text === identifier);
}
/**
* Gets all containing conditions for a node
*/
/* static findContainingConditions(node: ts.Node): string[] {
const conditions: string[] = [];
let current: ts.Node | undefined = node;
while (current) {
if (ts.isIfStatement(current)) {
conditions.push(current.expression.getText());
} else if (ts.isConditionalExpression(current)) {
conditions.push(current.condition.getText());
} else if (
ts.isBinaryExpression(current) &&
(current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
current.operatorToken.kind === ts.SyntaxKind.BarBarToken)
) {
conditions.push(current.getText());
}
current = current.parent;
}
return conditions;
} */
static findContainingConditions(node) {
const conditions = [];
let current = node;
while (current) {
// If statement conditions
if (typescript_1.default.isIfStatement(current)) {
conditions.push(current.expression.getText());
}
// Ternary expressions
else if (typescript_1.default.isConditionalExpression(current)) {
conditions.push(current.condition.getText());
}
// Logical expressions (&& and ||)
else if (typescript_1.default.isBinaryExpression(current)) {
if (current.operatorToken.kind ===
typescript_1.default.SyntaxKind.AmpersandAmpersandToken ||
current.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken ||
current.operatorToken.kind === typescript_1.default.SyntaxKind.QuestionQuestionToken) {
conditions.push(current.getText());
}
}
// JSX conditional rendering
else if (typescript_1.default.isJsxElement(current) ||
typescript_1.default.isJsxSelfClosingElement(current)) {
const conditionalAttrs = this.findJsxConditionalAttributes(current);
conditions.push(...conditionalAttrs);
}
// Handle switch statements
else if (typescript_1.default.isCaseClause(current) || typescript_1.default.isDefaultClause(current)) {
const switchStmt = current.parent.parent;
if (typescript_1.default.isSwitchStatement(switchStmt)) {
conditions.push(`${switchStmt.expression.getText()} === ${current.getText()}`);
}
}
current = current.parent;
}
return [...new Set(conditions)]; // Remove duplicates
}
static findJsxConditionalAttributes(node) {
const conditions = [];
// Get attributes based on node type
const attributes = typescript_1.default.isJsxElement(node)
? node.openingElement.attributes
: node.attributes;
// Process JSX attributes
attributes.properties.forEach((prop) => {
if (typescript_1.default.isJsxAttribute(prop)) {
// Handle conditional rendering props
if (prop.name.getText() === "hidden" ||
prop.name.getText() === "disabled" ||
prop.name.getText() === "show" ||
prop.name.getText() === "when" ||
prop.name.getText() === "if") {
if (prop.initializer &&
typescript_1.default.isJsxExpression(prop.initializer) &&
prop.initializer.expression) {
conditions.push(prop.initializer.expression.getText());
}
}
// Handle render props with conditions
if (prop.name.getText().startsWith("render") && prop.initializer) {
if (typescript_1.default.isJsxExpression(prop.initializer) &&
prop.initializer.expression) {
const expression = prop.initializer.expression;
if (typescript_1.default.isArrowFunction(expression) ||
typescript_1.default.isFunctionExpression(expression)) {
// Look for conditions within the render prop function
this.findConditionsInFunction(expression).forEach((cond) => conditions.push(cond));
}
}
}
}
});
return conditions;
}
static findConditionsInFunction(node) {
const conditions = [];
const visitor = (node) => {
if (typescript_1.default.isIfStatement(node)) {
conditions.push(node.expression.getText());
}
else if (typescript_1.default.isConditionalExpression(node)) {
conditions.push(node.condition.getText());
}
else if (typescript_1.default.isBinaryExpression(node) &&
(node.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken ||
node.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken ||
node.operatorToken.kind === typescript_1.default.SyntaxKind.QuestionQuestionToken)) {
conditions.push(node.getText());
}
typescript_1.default.forEachChild(node, visitor);
};
typescript_1.default.forEachChild(node, visitor);
return conditions;
}
/**
* Gets all identifiers used in a node
*/
static findIdentifiersInFiles(sourceFiles) {
const results = new Map();
sourceFiles.forEach((sourceFile, filePath) => {
const fileResults = this.findNodes(sourceFile, typescript_1.default.isIdentifier);
if (fileResults.length > 0) {
results.set(filePath, fileResults);
}
});
return results;
}
/**
* Gets the containing function-like declaration
*/
static getContainingFunction(node) {
return this.findNearestParent(node, (n) => typescript_1.default.isFunctionLike(n));
}
/**
* Gets the containing block scope
*/
static getContainingBlock(node) {
return this.findNearestParent(node, typescript_1.default.isBlock);
}
/**
* Checks if a node represents a pure expression
*/
static isPure(node) {
if (typescript_1.default.isLiteralExpression(node))
return true;
if (typescript_1.default.isIdentifier(node))
return true;
if (typescript_1.default.isArrayLiteralExpression(node)) {
return node.elements.every((e) => typescript_1.default.isExpression(e) && this.isPure(e));
}
if (typescript_1.default.isObjectLiteralExpression(node)) {
return node.properties.every((p) => typescript_1.default.isPropertyAssignment(p) && this.isPure(p.initializer));
}
return false;
}
/**
* Finds nodes across multiple files with source file context
*/
static findNodesWithContext(sourceFiles, predicate) {
const results = [];
sourceFiles.forEach((sourceFile, filePath) => {
function visit(node) {
if (predicate(node)) {
results.push({ node, sourceFile, filePath });
}
typescript_1.default.forEachChild(node, visit);
}
visit(sourceFile);
});
return results;
}
static isErrorCreation(node) {
if (typescript_1.default.isNewExpression(node)) {
const className = node.expression.getText();
return className.includes("Error");
}
return false;
}
static isPromiseRejection(node) {
if (typescript_1.default.isCallExpression(node) && typescript_1.default.isIdentifier(node.expression)) {
const name = node.expression.text;
return name === "reject" || name === "Promise.reject";
}
return false;
}
static isInsideCatchClause(node) {
let current = node.parent;
while (current) {
if (typescript_1.default.isCatchClause(current)) {
return true;
}
current = current.parent;
}
return false;
}
static isCustomErrorClass(node) {
if (typescript_1.default.isClassDeclaration(node)) {
const heritage = node.heritageClauses?.some((clause) => clause.types.some((type) => type.expression.getText().includes("Error")));
return !!heritage;
}
return false;
}
static getCustomErrorClassName(node) {
return node.name?.text;
}
static getFunctionNameFromNode(node) {
if (typescript_1.default.isFunctionDeclaration(node) && node.name) {
return node.name.text;
}
if (typescript_1.default.isMethodDeclaration(node) && typescript_1.default.isIdentifier(node.name)) {
return node.name.text;
}
if (typescript_1.default.isArrowFunction(node)) {
const parent = node.parent;
if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
return parent.name.text;
}
}
return "anonymous";
}
static safeGetNodeText(node) {
try {
return node?.getText() ?? "";
}
catch {
return "";
}
}
static safeIsArrayBindingPattern(node) {
try {
return !!node && typescript_1.default.isArrayBindingPattern(node);
}
catch {
return false;
}
}
static isTestFile(node) {
const sourceFile = node.getSourceFile();
const fileName = sourceFile.fileName.toLowerCase();
return (fileName.includes(".test.") ||
fileName.includes(".spec.") ||
fileName.includes("__tests__"));
}
static isHook(node) {
return (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isIdentifier(node.expression) &&
node.expression.text.startsWith("use") &&
node.expression.text !== "useState" &&
node.expression.text !== "useEffect");
}
static getHookName(node) {
if (typescript_1.default.isIdentifier(node.expression)) {
return node.expression.text;
}
return "anonymous-hook";
}
static isAnalyzableFunction(node, typeChecker // Add typeChecker parameter
) {
return ((typescript_1.default.isFunctionDeclaration(node) ||
typescript_1.default.isMethodDeclaration(node) ||
typescript_1.default.isArrowFunction(node)) &&
!(0, reactSpecific_1.isReactComponent)(node, typeChecker) && // Pass typeChecker
!this.isTestFile(node));
}
static isEventHandler(node) {
return ((typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isPropertyDeclaration(node)) &&
node.name &&
typescript_1.default.isIdentifier(node.name) &&
(node.name.text.startsWith("handle") || node.name.text.startsWith("on")));
}
static isPromiseRelated(node) {
if (typescript_1.default.isCallExpression(node)) {
const text = node.expression.getText();
return (text.includes("Promise") ||
text.includes("fetch") ||
text.includes("axios") ||
text.includes("request") ||
text.includes("query"));
}
return false;
}
}
exports.ASTUtils = ASTUtils;