UNPKG

@notjustcoders/ioc-arise

Version:

Arise type-safe IoC containers from your code. Zero overhead, zero coupling.

277 lines 16.4 kB
"use strict"; 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