@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
325 lines • 17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FactoryAnalyzer = void 0;
const path_1 = require("path");
const container_1 = require("../container");
const logger_1 = require("../utils/logger");
class FactoryAnalyzer {
constructor(sourceDir, factoryPattern) {
this.astParser = container_1.container.astParser;
this.sourceDir = sourceDir;
if (factoryPattern) {
try {
this.factoryPattern = new RegExp(factoryPattern, 'i');
}
catch (error) {
logger_1.Logger.warn(`Invalid factory pattern regex: ${factoryPattern}. Using default behavior.`);
}
}
}
async collectTokens(filePaths) {
const factoryNames = new Set();
const interfaces = 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 factory names (exported functions with @factory or pattern match)
const functionNodes = this.astParser.findAllFunctions(root);
for (const functionNode of functionNodes) {
const functionName = this.astParser.extractFunctionName(functionNode);
if (functionName) {
const isExported = this.astParser.isExportedFunction(functionNode);
const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode);
const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false;
const returnType = this.astParser.extractFunctionReturnType(functionNode);
const instanceFactoryFor = returnType ? this.extractCoreInterfaceName(returnType, interfaces) : undefined;
if (isExported && (hasFactoryAnnotation || matchesFactoryPattern || instanceFactoryFor)) {
factoryNames.add(functionName);
if (instanceFactoryFor) {
interfaces.add(instanceFactoryFor);
}
}
}
}
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not collect tokens from ${filePath}:`, { error });
}
}
return { factoryNames, interfaces };
}
async analyzeFiles(filePaths, tokens) {
const allFactories = [];
let allInterfaces = tokens?.interfaces || new Set();
let allClassNames = tokens?.classNames || new Set();
let allFactoryNames = tokens?.factoryNames || new Set();
const fileASTMap = new Map();
if (!tokens) {
// First pass: Collect types for dependency resolution
for (const filePath of filePaths) {
try {
const root = this.astParser.parseFile(filePath);
fileASTMap.set(filePath, root);
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));
const classNodes = this.astParser.findAllClasses(root);
for (const classNode of classNodes) {
const className = this.astParser.extractClassName(classNode);
if (className) {
allClassNames.add(className);
}
}
// Collect factory names (exported functions with @factory or pattern match)
const functionNodes = this.astParser.findAllFunctions(root);
for (const functionNode of functionNodes) {
const functionName = this.astParser.extractFunctionName(functionNode);
if (functionName) {
const isExported = this.astParser.isExportedFunction(functionNode);
const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode);
const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false;
const returnType = this.astParser.extractFunctionReturnType(functionNode);
const instanceFactoryFor = returnType ? this.extractCoreInterfaceName(returnType, allInterfaces) : undefined;
if (isExported && (hasFactoryAnnotation || matchesFactoryPattern || instanceFactoryFor)) {
allFactoryNames.add(functionName);
}
}
}
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not parse ${filePath}:`, { error });
}
}
}
// Second pass: Analyze factory functions
for (const filePath of filePaths) {
let root = fileASTMap.get(filePath);
if (!root) {
try {
root = this.astParser.parseFile(filePath);
}
catch (e) {
continue;
}
}
if (root) {
const factories = await this.analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allFactoryNames);
allFactories.push(...factories);
}
}
return allFactories;
}
async analyzeFileFromAST(filePath, root, allInterfaces, allClassNames, allFactoryNames) {
const factories = [];
try {
const importMappings = this.astParser.extractImportMappings(root);
const jsDocScopes = this.astParser.extractJSDocComments(root);
const functionNodes = this.astParser.findAllFunctions(root);
logger_1.Logger.debug(`Found ${functionNodes.length} function nodes in ${filePath}`);
for (const functionNode of functionNodes) {
const functionName = this.astParser.extractFunctionName(functionNode);
logger_1.Logger.debug(`Extracted function name: ${functionName} from node: ${functionNode.text().substring(0, 100)}`);
if (!functionName) {
continue;
}
// Only analyze exported functions that are factories
// Factory functions are detected by:
// 1. @factory JSDoc comment (default behavior)
// 2. Function name matching factoryPattern regex (if configured)
const isExported = this.astParser.isExportedFunction(functionNode);
const hasFactoryAnnotation = this.hasFactoryAnnotation(root, functionNode);
const matchesFactoryPattern = this.factoryPattern ? this.factoryPattern.test(functionName) : false;
// Detect instance factories: exported functions with an explicit return type
// matching a known interface (e.g., ): IUserRepository {)
const returnType = this.astParser.extractFunctionReturnType(functionNode);
const instanceFactoryFor = returnType
? this.extractCoreInterfaceName(returnType, allInterfaces)
: undefined;
logger_1.Logger.debug(`Function ${functionName}: isExported=${isExported}, hasFactoryAnnotation=${hasFactoryAnnotation}, matchesFactoryPattern=${matchesFactoryPattern}, instanceFactoryFor=${instanceFactoryFor}`);
if (!isExported || (!hasFactoryAnnotation && !matchesFactoryPattern && !instanceFactoryFor)) {
logger_1.Logger.debug(`Skipping function ${functionName}`);
continue;
}
const parameters = this.astParser.extractFunctionParameters(functionNode);
// Instance factories default to transient (they produce new instances on demand).
// Regular @factory / pattern-matched factories default to singleton.
// Either default can be overridden with /** @scope transient|singleton */
const defaultScope = instanceFactoryFor ? 'transient' : 'singleton';
const scope = jsDocScopes.get(functionName) || defaultScope;
// Check if this is a context object pattern (single parameter with object type)
const isContextObjectPattern = parameters.length === 1 &&
parameters[0] !== undefined &&
parameters[0].type.startsWith('{') &&
parameters[0].type.includes(':');
let dependencies = [];
let contextObjectName;
let contextObjectProperties;
if (isContextObjectPattern && parameters[0]) {
// Extract properties from context object
const contextParam = parameters[0];
contextObjectName = contextParam.name;
contextObjectProperties = this.astParser.extractObjectTypeProperties(functionNode, contextParam.name);
// Resolve dependencies from context object properties
dependencies = this.resolveDependenciesFromContextProperties(contextObjectProperties || [], allInterfaces, allClassNames, allFactoryNames, importMappings);
}
else {
// Resolve dependencies from individual parameters (existing behavior)
dependencies = this.resolveDependencies(parameters, allInterfaces, allClassNames, allFactoryNames, importMappings);
}
// Calculate import path
const importPath = this.calculateImportPath(filePath);
// Determine token:
// - Instance factories use the interface name as the token (they are implementation providers)
// - Regular factories use the function name as the token
const token = instanceFactoryFor || functionName;
factories.push({
name: functionName,
filePath,
dependencies,
parameters,
returnType,
importPath,
scope,
token,
useContextObject: isContextObjectPattern,
contextObjectName,
contextObjectProperties,
instanceFactoryFor,
});
}
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not analyze factories in ${filePath}:`, { error });
}
return factories;
}
hasFactoryAnnotation(root, functionNode) {
try {
// Find all JSDoc comments in the file
const comments = root.findAll({
rule: {
kind: 'comment'
}
});
const functionRange = functionNode.range();
const functionStartLine = functionRange.start.line;
// Find the JSDoc comment that appears immediately before this function
for (const comment of comments) {
const commentText = comment.text();
if (commentText.includes('/**') && commentText.includes('@factory')) {
const commentRange = comment.range();
const commentEndLine = commentRange.end.line;
// Check if the comment comes before the function (within a few lines)
if (commentEndLine < functionStartLine &&
functionStartLine - commentEndLine <= 2) {
return true;
}
}
}
return false;
}
catch {
return false;
}
}
resolveDependencies(parameters, allInterfaces, allClassNames, allFactoryNames, importMappings) {
const dependencies = [];
for (const param of parameters) {
const typeName = param.type;
// Skip primitive types
if (['string', 'number', 'boolean', 'Date', 'Object', 'Array'].includes(typeName)) {
continue;
}
// Skip object types (they're handled separately)
if (typeName.startsWith('{')) {
continue;
}
// Determine if it's an interface, class, or factory
const isInterface = allInterfaces.has(typeName);
const isClass = allClassNames.has(typeName);
const isFactory = allFactoryNames.has(typeName);
// Also accept types imported directly from external packages (non-relative path)
const rawImportPath = importMappings.get(typeName);
const isExternalImport = rawImportPath !== undefined && !rawImportPath.startsWith('.');
if (isInterface || isClass || isFactory || isExternalImport) {
const importPath = rawImportPath || './';
dependencies.push({
name: typeName,
importPath,
});
}
}
return dependencies;
}
resolveDependenciesFromContextProperties(properties, allInterfaces, allClassNames, allFactoryNames, importMappings) {
const dependencies = [];
for (const prop of properties) {
const typeName = prop.type;
// Skip primitive types
if (['string', 'number', 'boolean', 'Date', 'Object', 'Array'].includes(typeName)) {
continue;
}
// Determine if it's an interface, class, or factory
const isInterface = allInterfaces.has(typeName);
const isClass = allClassNames.has(typeName);
const isFactory = allFactoryNames.has(typeName);
// Also accept types imported directly from external packages (non-relative path)
const rawImportPath = importMappings.get(typeName);
const isExternalImport = rawImportPath !== undefined && !rawImportPath.startsWith('.');
if (isInterface || isClass || isFactory || isExternalImport) {
const importPath = rawImportPath || './';
dependencies.push({
name: typeName,
importPath,
});
}
}
return dependencies;
}
calculateImportPath(filePath) {
const relativePath = (0, path_1.relative)(this.sourceDir, filePath);
// Convert to import path format (remove .ts extension, handle directory separators)
return relativePath
.replace(/\.ts$/, '')
.replace(/\\/g, '/')
.replace(/^\.\//, '');
}
/**
* Extracts the core interface name from a return type annotation.
* Handles: IFoo, Promise<IFoo>, Awaited<IFoo>, Awaited<Promise<IFoo>>
* Returns the interface name if it matches a known interface, otherwise null.
*/
extractCoreInterfaceName(returnType, allInterfaces) {
const trimmed = returnType.trim();
// Direct: IFoo
if (allInterfaces.has(trimmed)) {
return trimmed;
}
// Promise<IFoo>
const promiseMatch = trimmed.match(/^Promise\s*<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>$/);
if (promiseMatch?.[1] && allInterfaces.has(promiseMatch[1])) {
return promiseMatch[1];
}
// Awaited<IFoo> or Awaited<Promise<IFoo>>
const awaitedMatch = trimmed.match(/^Awaited\s*<\s*(?:Promise\s*<\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*>?\s*>$/);
if (awaitedMatch?.[1] && allInterfaces.has(awaitedMatch[1])) {
return awaitedMatch[1];
}
return undefined;
}
}
exports.FactoryAnalyzer = FactoryAnalyzer;
//# sourceMappingURL=factory-analyzer.js.map