@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
330 lines • 18.9 kB
JavaScript
"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