@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
277 lines • 16.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClassAnalyzer = void 0;
const path_1 = require("path");
const container_1 = require("../container");
const one_logger_client_sdk_1 = require("@notjustcoders/one-logger-client-sdk");
const index_js_1 = require("../errors/index.js");
class ClassAnalyzer {
constructor(sourceDir, interfacePattern) {
this.astParser = container_1.container.astParser;
this.sourceDir = sourceDir;
this.interfacePattern = interfacePattern;
}
async analyzeFiles(filePaths) {
const allClasses = [];
const allInterfaces = new Set();
const allClassNames = new Set();
const allAbstractClasses = new Set();
const fileASTMap = new Map();
// First pass: Parse all files and collect interface, class names, and abstract classes
for (const filePath of filePaths) {
try {
const root = this.astParser.parseFile(filePath);
fileASTMap.set(filePath, root);
// Collect interfaces
const interfaces = this.astParser.extractInterfaces(root);
interfaces.forEach(interfaceName => allInterfaces.add(interfaceName));
// Collect class names and abstract classes
const classNodes = this.astParser.findAllClasses(root);
console.log(`DEBUG: Found ${classNodes.length} class nodes in ${filePath}`);
for (const classNode of classNodes) {
const className = this.astParser.extractClassName(classNode);
if (className) {
allClassNames.add(className);
const isAbstract = this.astParser.isAbstractClass(classNode);
console.log(`DEBUG: Class ${className} - isAbstract: ${isAbstract}`);
console.log(`DEBUG: Class node text:`, classNode.text().substring(0, 100));
if (isAbstract) {
allAbstractClasses.add(className);
}
}
}
}
catch (error) {
one_logger_client_sdk_1.logger.warn(`Warning: Could not parse ${filePath}:`, { error });
}
}
// Second pass: Analyze each file using the cached ASTs and collected types
for (const filePath of filePaths) {
const root = fileASTMap.get(filePath);
if (root) {
const classes = await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses);
allClasses.push(...classes);
}
}
// Filter out classes that have zero dependencies and are not referenced by others
const filteredClasses = this.filterUnusedClasses(allClasses);
return filteredClasses;
}
async analyzeFile(filePath, allInterfaces, allClassNames, allAbstractClasses) {
try {
const root = this.astParser.parseFile(filePath);
return await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses);
}
catch (error) {
one_logger_client_sdk_1.logger.warn(`Warning: Could not parse ${filePath}:`, { error });
return [];
}
}
async analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses) {
const classes = [];
try {
// Extract import aliases for type resolution
const typeAliases = this.astParser.extractTypeAliases(root);
// Extract import mappings for dependency paths
const importMappings = this.astParser.extractImportMappings(root);
// Extract JSDoc scope annotations at file level for this specific file
const jsDocScopes = this.astParser.extractJSDocComments(root);
one_logger_client_sdk_1.logger.log('JSDoc scopes for file', { filePath, jsDocScopes });
// Find all classes once and process them efficiently
const allClassNodes = this.astParser.findAllClasses(root);
const classNodesWithInterfaces = this.astParser.findClassesImplementingInterfaces(root);
const classNodesExtendingClasses = this.astParser.findClassesExtendingClasses(root);
const processedClassNames = new Set();
// Process classes with interfaces first
for (const classNode of classNodesWithInterfaces) {
const className = this.astParser.extractClassName(classNode);
const interfaceName = this.astParser.extractInterfaceName(classNode);
if (!className || !interfaceName)
continue;
// Filter by interface pattern if specified
if (this.interfacePattern) {
const pattern = new RegExp(this.interfacePattern);
if (!pattern.test(interfaceName))
continue;
}
const constructorParams = this.astParser.extractConstructorParameters(classNode);
const dependencies = this.resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses);
const importPath = this.generateImportPath(filePath, className);
const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes);
const isAbstract = this.astParser.isAbstractClass(classNode);
processedClassNames.add(className);
one_logger_client_sdk_1.logger.log(`Processing class with interface: ${className}`);
one_logger_client_sdk_1.logger.log(`Interface for ${className}:`, { interfaceName });
one_logger_client_sdk_1.logger.log(`Constructor params for ${className}:`, { constructorParams });
one_logger_client_sdk_1.logger.log(`Type aliases found:`, { count: Array.from(typeAliases.entries()) });
one_logger_client_sdk_1.logger.log(`Dependencies for ${className}:`, { dependencies });
one_logger_client_sdk_1.logger.log(`Scope for ${className}:`, { scope });
one_logger_client_sdk_1.logger.log(`Is abstract: ${isAbstract}`);
// Skip abstract classes - they cannot be instantiated
if (isAbstract) {
one_logger_client_sdk_1.logger.log(`Skipping abstract class: ${className} (abstract classes cannot be instantiated)`);
continue;
}
classes.push({
name: className,
filePath,
dependencies,
constructorParams,
interfaceName,
abstractClassName: undefined, // No abstract class for interface implementations
importPath,
scope
});
}
// Process classes extending other classes
for (const classNode of classNodesExtendingClasses) {
const className = this.astParser.extractClassName(classNode);
const parentClassName = this.astParser.extractParentClassName(classNode);
if (!className || !parentClassName || processedClassNames.has(className))
continue;
// Check if the parent class is abstract using the global abstract classes set
const isParentAbstract = allAbstractClasses.has(parentClassName);
console.log(`DEBUG: Class ${className} extends ${parentClassName}. Is parent abstract? ${isParentAbstract}`);
console.log(`DEBUG: All abstract classes:`, Array.from(allAbstractClasses));
// Only process if parent is abstract
if (isParentAbstract) {
const constructorParams = this.astParser.extractConstructorParameters(classNode);
const dependencies = this.resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses);
const importPath = this.generateImportPath(filePath, className);
const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes);
const isAbstract = this.astParser.isAbstractClass(classNode);
processedClassNames.add(className);
one_logger_client_sdk_1.logger.log(`Processing class extending ${isParentAbstract ? 'abstract ' : ''}class: ${className} extends ${parentClassName}`);
one_logger_client_sdk_1.logger.log(`Constructor params for ${className}:`, { constructorParams });
one_logger_client_sdk_1.logger.log(`Dependencies for ${className}:`, { dependencies });
one_logger_client_sdk_1.logger.log(`Scope for ${className}:`, { scope });
one_logger_client_sdk_1.logger.log(`Is abstract: ${isAbstract}`);
// Skip abstract classes - they cannot be instantiated
if (isAbstract) {
one_logger_client_sdk_1.logger.log(`Skipping abstract class: ${className} (abstract classes cannot be instantiated)`);
continue;
}
classes.push({
name: className,
filePath,
dependencies,
constructorParams,
interfaceName: undefined, // No interface for classes extending abstract classes
abstractClassName: parentClassName, // Track which abstract class this extends
importPath,
scope
});
one_logger_client_sdk_1.logger.log(`Added class ${className} extending abstract class: ${parentClassName}`);
}
}
// Process remaining classes without interfaces or inheritance
for (const classNode of allClassNodes) {
const className = this.astParser.extractClassName(classNode);
if (!className || processedClassNames.has(className))
continue;
// Skip if this class implements an interface or extends another class (already processed above)
const classText = classNode.text();
if (classText.includes('implements') || classText.includes('extends'))
continue;
const constructorParams = this.astParser.extractConstructorParameters(classNode);
const dependencies = this.resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses);
const importPath = this.generateImportPath(filePath, className);
const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes);
const isAbstract = this.astParser.isAbstractClass(classNode);
one_logger_client_sdk_1.logger.log(`Processing class without interface: ${className}`);
one_logger_client_sdk_1.logger.log(`Constructor params for ${className}:`, { constructorParams });
one_logger_client_sdk_1.logger.log(`Dependencies for ${className}:`, { dependencies });
one_logger_client_sdk_1.logger.log(`Scope for ${className}:`, { scope });
one_logger_client_sdk_1.logger.log(`Is abstract: ${isAbstract}`);
// Skip abstract classes - they cannot be instantiated
if (isAbstract) {
one_logger_client_sdk_1.logger.log(`Skipping abstract class: ${className} (abstract classes cannot be instantiated)`);
continue;
}
classes.push({
name: className,
filePath,
dependencies,
constructorParams,
interfaceName: undefined, // No interface for these classes
abstractClassName: undefined, // No abstract class for standalone classes
importPath,
scope
});
}
}
catch (error) {
throw index_js_1.ErrorFactory.wrapError(error, index_js_1.IoCErrorCode.ANALYSIS_FAILED, { filePath });
}
return classes;
}
resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses) {
const result = constructorParams
.map(param => {
// Resolve type aliases to actual interface names
const resolvedType = typeAliases.get(param.type) || param.type;
return {
originalType: param.type,
resolvedType: resolvedType
};
})
.filter(typeInfo => {
console.log("all interfaces", allInterfaces);
console.log("all classes", allClassNames);
console.log("all abstract classes", allAbstractClasses);
const isValidInterface = allInterfaces.has(typeInfo.resolvedType);
const isValidClass = allClassNames.has(typeInfo.resolvedType);
const isValidAbstractClass = allAbstractClasses?.has(typeInfo.resolvedType) || false;
return isValidClass || isValidInterface || isValidAbstractClass;
})
.map(typeInfo => {
// Get import path from import mappings, fallback to resolvedType if not found
const importPath = importMappings.get(typeInfo.originalType) ||
importMappings.get(typeInfo.resolvedType) ||
`./${typeInfo.resolvedType}`; // fallback for local types
return {
name: typeInfo.resolvedType,
importPath: importPath
};
});
return result;
}
filterUnusedClasses(classes) {
// Create a set of all dependencies (class names and interface names referenced by others)
const referencedTypes = new Set();
classes.forEach(classInfo => {
classInfo.dependencies.forEach(dep => {
referencedTypes.add(dep.name);
});
});
// Filter out classes that:
// 1. Have zero dependencies AND are not referenced by any other class
// Note: Controllers and other entry points should be kept even if not referenced
// Note: Abstract classes are already excluded during collection phase
const filteredClasses = classes.filter(classInfo => {
const isClassReferenced = referencedTypes.has(classInfo.name);
const isInterfaceReferenced = classInfo.interfaceName ? referencedTypes.has(classInfo.interfaceName) : false;
const isAbstractClassReferenced = classInfo.abstractClassName ? referencedTypes.has(classInfo.abstractClassName) : false;
// Keep the class if:
// - It has dependencies (including controllers that inject services), OR
// - It is referenced by others (either by class name or interface name), OR
// - It extends an abstract class that is referenced by others
const shouldKeep = classInfo.dependencies.length > 0 || isClassReferenced || isInterfaceReferenced || isAbstractClassReferenced;
// Debug logging removed for cleaner output
if (!shouldKeep) {
one_logger_client_sdk_1.logger.log(`Filtering out unused class: ${classInfo.name} (no dependencies and not referenced by others)`);
}
return shouldKeep;
});
one_logger_client_sdk_1.logger.log(`Filtered classes: ${classes.length} -> ${filteredClasses.length}`);
return filteredClasses;
}
generateImportPath(filePath, className) {
// Generate relative import path from the output directory
const relativePath = (0, path_1.relative)(this.sourceDir, filePath);
const pathWithoutExtension = relativePath.replace(/\.ts$/, '');
return `./${pathWithoutExtension}`;
}
}
exports.ClassAnalyzer = ClassAnalyzer;
//# sourceMappingURL=class-analyzer.js.map