UNPKG

@notjustcoders/ioc-arise

Version:

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

330 lines 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClassAnalyzer = void 0; const path_1 = require("path"); const container_1 = require("../container"); const errorFactory_1 = require("../errors/errorFactory"); const IoCError_1 = require("../errors/IoCError"); const logger_1 = require("../utils/logger"); class ClassAnalyzer { constructor(sourceDir, interfacePattern) { this.astParser = container_1.container.astParser; this.sourceDir = sourceDir; this.interfacePattern = interfacePattern; } async collectTokens(filePaths) { const interfaces = new Set(); const classNames = new Set(); const abstractClasses = new Set(); for (const filePath of filePaths) { try { const root = this.astParser.parseFile(filePath); // Collect interfaces const extractedInterfaces = this.astParser.extractInterfaces(root); extractedInterfaces.forEach(i => interfaces.add(i)); // Collect type aliases const typeAliases = this.astParser.extractTypeAliases(root); for (const [alias] of typeAliases.entries()) { interfaces.add(alias); } // Collect names re-exported from external packages (e.g. from a monorepo sibling) const externalReExports = this.astParser.extractExternalReExports(root); externalReExports.forEach(name => interfaces.add(name)); // Collect class names and abstract classes const classNodes = this.astParser.findAllClasses(root); for (const classNode of classNodes) { const className = this.astParser.extractClassName(classNode); if (className) { classNames.add(className); if (this.astParser.isAbstractClass(classNode)) { abstractClasses.add(className); } } } } catch (error) { logger_1.Logger.warn(`Warning: Could not collect tokens from ${filePath}:`, { error }); } } return { interfaces, classNames, abstractClasses }; } async analyzeFiles(filePaths, tokens) { const allClasses = []; // If tokens are provided, use them; otherwise, perform a first pass (legacy behavior) let allInterfaces = tokens?.interfaces || new Set(); let allClassNames = tokens?.classNames || new Set(); let allAbstractClasses = tokens?.abstractClasses || new Set(); let allFactoryNames = tokens?.factoryNames || new Set(); let allValueNames = tokens?.valueNames || new Set(); const fileASTMap = new Map(); if (!tokens) { // 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 names re-exported from external packages const externalReExports = this.astParser.extractExternalReExports(root); externalReExports.forEach(name => allInterfaces.add(name)); // Collect class names and abstract classes const classNodes = this.astParser.findAllClasses(root); for (const classNode of classNodes) { const className = this.astParser.extractClassName(classNode); if (className) { allClassNames.add(className); const isAbstract = this.astParser.isAbstractClass(classNode); if (isAbstract) { allAbstractClasses.add(className); } } } } catch (error) { logger_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) { let root = fileASTMap.get(filePath); if (!root) { try { root = this.astParser.parseFile(filePath); } catch (e) { continue; } } if (root) { const classes = await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses, allFactoryNames, allValueNames); allClasses.push(...classes); } } // Don't filter out unused classes - we need ALL implementing classes for: // 1. Duplicate interface detection // 2. User might want to register services that aren't used yet // 3. Entry points (controllers, etc.) might not be referenced return allClasses; } async analyzeFile(filePath, allInterfaces, allClassNames, allAbstractClasses) { try { const root = this.astParser.parseFile(filePath); return await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses); } catch (error) { logger_1.Logger.warn(`Warning: Could not parse ${filePath}:`, { error }); return []; } } async analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allAbstractClasses, allFactoryNames = new Set(), allValueNames = new Set()) { 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); logger_1.Logger.debug('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, allFactoryNames, allValueNames); const importPath = this.generateImportPath(filePath, className); const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes); const isAbstract = this.astParser.isAbstractClass(classNode); processedClassNames.add(className); logger_1.Logger.debug(`Processing class with interface: ${className}`); logger_1.Logger.debug(`Interface for ${className}:`, { interfaceName }); logger_1.Logger.debug(`Constructor params for ${className}:`, { constructorParams }); logger_1.Logger.debug(`Type aliases found:`, { count: Array.from(typeAliases.entries()) }); logger_1.Logger.debug(`Dependencies for ${className}:`, { dependencies }); logger_1.Logger.debug(`Scope for ${className}:`, { scope }); logger_1.Logger.debug(`Is abstract: ${isAbstract}`); // Skip abstract classes - they cannot be instantiated if (isAbstract) { logger_1.Logger.debug(`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); // Only process if parent is abstract if (isParentAbstract) { const constructorParams = this.astParser.extractConstructorParameters(classNode); const dependencies = this.resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses, allFactoryNames, allValueNames); const importPath = this.generateImportPath(filePath, className); const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes); const isAbstract = this.astParser.isAbstractClass(classNode); processedClassNames.add(className); logger_1.Logger.debug(`Processing class extending ${isParentAbstract ? 'abstract ' : ''}class: ${className} extends ${parentClassName}`); logger_1.Logger.debug(`Constructor params for ${className}:`, { constructorParams }); logger_1.Logger.debug(`Dependencies for ${className}:`, { dependencies }); logger_1.Logger.debug(`Scope for ${className}:`, { scope }); logger_1.Logger.debug(`Is abstract: ${isAbstract}`); // Skip abstract classes - they cannot be instantiated if (isAbstract) { logger_1.Logger.debug(`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 }); logger_1.Logger.debug(`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, allFactoryNames, allValueNames); const importPath = this.generateImportPath(filePath, className); const scope = this.astParser.extractScopeFromJSDoc(className, jsDocScopes); const isAbstract = this.astParser.isAbstractClass(classNode); logger_1.Logger.debug(`Processing class without interface: ${className}`); logger_1.Logger.debug(`Constructor params for ${className}:`, { constructorParams }); logger_1.Logger.debug(`Dependencies for ${className}:`, { dependencies }); logger_1.Logger.debug(`Scope for ${className}:`, { scope }); logger_1.Logger.debug(`Is abstract: ${isAbstract}`); // Skip abstract classes - they cannot be instantiated if (isAbstract) { logger_1.Logger.debug(`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 errorFactory_1.ErrorFactory.wrapError(error, IoCError_1.IoCErrorCode.ANALYSIS_FAILED, { filePath }); } return classes; } resolveDependencies(constructorParams, typeAliases, importMappings, allInterfaces, allClassNames, allAbstractClasses = new Set(), allFactoryNames = new Set(), allValueNames = new Set()) { 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 => { const isValidInterface = allInterfaces.has(typeInfo.resolvedType); const isValidClass = allClassNames.has(typeInfo.resolvedType); const isValidAbstractClass = allAbstractClasses.has(typeInfo.resolvedType); const isValidFactory = allFactoryNames.has(typeInfo.resolvedType); const isValidValue = allValueNames.has(typeInfo.resolvedType); // Also accept types imported directly from external packages (non-relative path). // This covers: import type { IFoo } from 'some-package' const rawImportPath = importMappings.get(typeInfo.originalType) || importMappings.get(typeInfo.resolvedType); const isExternalImport = rawImportPath !== undefined && !rawImportPath.startsWith('.'); return isValidClass || isValidInterface || isValidAbstractClass || isValidFactory || isValidValue || isExternalImport; }) .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) { logger_1.Logger.debug(`Filtering out unused class: ${classInfo.name} (no dependencies and not referenced by others)`); } return shouldKeep; }); logger_1.Logger.debug(`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