@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
820 lines • 35.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASTParser = void 0;
const fs_1 = require("fs");
const napi_1 = require("@ast-grep/napi");
const errorFactory_1 = require("../errors/errorFactory");
const logger_1 = require("../utils/logger");
class ASTParser {
parseFile(filePath) {
try {
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
const ast = napi_1.ts.parse(content);
return ast.root();
}
catch (error) {
throw errorFactory_1.ErrorFactory.fileReadError(filePath, error instanceof Error ? error.message : String(error));
}
}
findClassesImplementingInterfaces(root) {
return root.findAll({
rule: {
pattern: 'class $CLASS implements $INTERFACE { $$$ }'
}
});
}
findClassesExtendingClasses(root) {
return root.findAll({
rule: {
pattern: 'class $CLASS extends $PARENT { $$$ }'
}
});
}
findAllClasses(root) {
// Find regular class declarations
const regularClasses = root.findAll({
rule: {
kind: 'class_declaration'
}
});
// Find abstract class declarations using pattern matching
const abstractClasses = root.findAll({
rule: {
pattern: 'abstract class $NAME { $$$ }'
}
});
// Also try to find exported abstract classes
const exportedAbstractClasses = root.findAll({
rule: {
pattern: 'export abstract class $NAME { $$$ }'
}
});
// Combine all found classes
return [...regularClasses, ...abstractClasses, ...exportedAbstractClasses];
}
extractClassName(classNode) {
logger_1.Logger.debug('Extracting class name from node:', { result: classNode.text().substring(0, 100) + '...' });
// Try different approaches to extract class name
let className = classNode.getMatch('CLASS')?.text();
if (className) {
logger_1.Logger.debug('Found class name using CLASS match:', { className });
return className;
}
// Try to match the class declaration pattern directly
const classText = classNode.text();
const classMatch = classText.match(/class\s+([A-Za-z_][A-Za-z0-9_]*)/);
if (classMatch) {
className = classMatch[1];
logger_1.Logger.debug('Found class name using regex:', { className });
return className;
}
logger_1.Logger.debug('Failed to extract class name');
return undefined;
}
extractInterfaceName(classNode) {
return classNode.getMatch('INTERFACE')?.text();
}
extractParentClassName(classNode) {
return classNode.getMatch('PARENT')?.text();
}
isAbstractClass(classNode) {
const classText = classNode.text();
const isAbstract = classText.includes('abstract class');
return isAbstract;
}
extractConstructorParameters(classNode) {
const parameters = [];
try {
// Find constructor within the class
const constructorNodes = classNode.findAll({
rule: {
kind: 'method_definition',
pattern: "$NAME",
regex: "^constructor"
}
});
logger_1.Logger.debug(`Found ${constructorNodes.length} constructor nodes`);
if (constructorNodes.length === 0) {
return parameters;
}
const constructorNode = constructorNodes[0];
// Find formal parameters within the constructor
const parameterNodes = constructorNode.findAll({
rule: {
kind: 'formal_parameters'
}
});
if (parameterNodes.length === 0) {
return parameters;
}
const formalParams = parameterNodes[0];
// Find individual parameters (both required and optional)
const requiredParamNodes = formalParams.findAll({
rule: {
kind: 'required_parameter'
}
});
const optionalParamNodes = formalParams.findAll({
rule: {
kind: 'optional_parameter'
}
});
const paramNodes = [...requiredParamNodes, ...optionalParamNodes];
logger_1.Logger.debug(`Found ${paramNodes.length} parameter nodes`);
for (const paramNode of paramNodes) {
const paramText = paramNode.text();
logger_1.Logger.debug('Parameter text:', paramText);
// Parse parameter using regex to extract details
const paramMatch = paramText.match(/(?:(private|public|protected)\s+)?(\w+)(\?)?\s*:\s*(\w+)/);
if (paramMatch) {
const [, accessModifier, name, optional, type] = paramMatch;
parameters.push({
name: name.trim(),
type: type.trim(),
isOptional: !!optional,
accessModifier: accessModifier
});
logger_1.Logger.debug(`Extracted parameter: ${name}: ${type}${optional ? '?' : ''} (${accessModifier || 'none'})`);
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract constructor parameters:', { error });
}
logger_1.Logger.debug('Final parameters:', { parameters });
return parameters;
}
extractJSDocComments(root) {
const entityScopes = new Map();
try {
const comments = root.findAll({ rule: { kind: 'comment' } });
logger_1.Logger.debug(`Found ${comments.length} comments in file`);
// Build a sorted list of all named entities: classes, functions, and const/let declarations.
// We sort by start line so we can find the first entity that follows each @scope comment.
const entities = [];
// Class declarations
const classDeclarations = root.findAll({ rule: { kind: 'class_declaration' } });
logger_1.Logger.debug(`Found ${classDeclarations.length} class declarations in file`);
for (const classDecl of classDeclarations) {
const name = this.extractClassName(classDecl);
if (name)
entities.push({ name, startLine: classDecl.range().start.line });
}
// Function declarations (export function foo() { ... })
const functionDeclarations = root.findAll({ rule: { kind: 'function_declaration' } });
logger_1.Logger.debug(`Found ${functionDeclarations.length} function declarations in file`);
for (const funcDecl of functionDeclarations) {
const name = this.extractFunctionName(funcDecl);
if (name)
entities.push({ name, startLine: funcDecl.range().start.line });
}
// Lexical declarations (export const foo = ... / export const foo: IFoo = ...)
const lexicalDeclarations = root.findAll({ rule: { kind: 'lexical_declaration' } });
logger_1.Logger.debug(`Found ${lexicalDeclarations.length} lexical declarations in file`);
for (const lexDecl of lexicalDeclarations) {
const text = lexDecl.text();
const varMatch = text.match(/(?:const|let)\s+([A-Za-z_][A-Za-z0-9_]*)/);
if (varMatch?.[1])
entities.push({ name: varMatch[1], startLine: lexDecl.range().start.line });
}
// Sort ascending by line so binary/linear search gives "first entity after comment"
entities.sort((a, b) => a.startLine - b.startLine);
for (const comment of comments) {
const commentText = comment.text();
logger_1.Logger.debug(`Comment text: ${commentText}`);
if (!commentText.includes('/**') || !commentText.includes('@scope'))
continue;
logger_1.Logger.debug(`Found @scope comment: ${commentText}`);
const scopeMatch = commentText.match(/@scope\s+(singleton|transient)/);
if (!scopeMatch)
continue;
const scope = scopeMatch[1];
const commentEndLine = comment.range().end.line;
logger_1.Logger.debug(`Comment end line: ${commentEndLine}, scope: ${scope}`);
// First entity whose start line is strictly after the comment's last line
const entity = entities.find(e => e.startLine > commentEndLine);
if (entity) {
logger_1.Logger.debug(`Found JSDoc scope annotation: ${entity.name} -> ${scope}`);
entityScopes.set(entity.name, scope);
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract JSDoc comments:', { error });
}
logger_1.Logger.debug(`Final entityScopes map:`, { entityScopes });
return entityScopes;
}
extractScopeFromJSDoc(className, jsDocScopes) {
return jsDocScopes.get(className) || 'singleton';
}
extractTypeAliases(root) {
const typeAliases = new Map();
try {
// 1. Find all import statements and parse 'as' aliases
const allImports = root.findAll({
rule: {
kind: 'import_statement'
}
});
for (const importNode of allImports) {
const importText = importNode.text();
const aliasMatch = importText.match(/import\s*{\s*([\w]+)\s+as\s+([\w]+)\s*}\s+from/);
if (aliasMatch) {
const [, original, alias] = aliasMatch;
typeAliases.set(alias.trim(), original.trim());
}
}
// 2. Find type alias declarations (export type Foo = ...)
const typeDeclarations = root.findAll({
rule: {
kind: 'type_alias_declaration'
}
});
for (const typeDecl of typeDeclarations) {
const typeText = typeDecl.text();
// Match: export type Name = ... or type Name = ...
const typeMatch = typeText.match(/(?:export\s+)?type\s+([\w]+)\s*=/);
if (typeMatch) {
const [, name] = typeMatch;
// For type aliases, we map them to themselves so they are recognized as valid tokens
typeAliases.set(name.trim(), name.trim());
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract type aliases:', { error });
}
return typeAliases;
}
extractImportMappings(root) {
const importMappings = new Map();
try {
// Find all import statements
const allImports = root.findAll({
rule: {
kind: 'import_statement'
}
});
logger_1.Logger.debug(`Found ${allImports.length} import statements for mapping`);
for (const importNode of allImports) {
const importText = importNode.text();
logger_1.Logger.debug(`Processing import: ${importText}`);
// Extract import path
const pathMatch = importText.match(/from\s+['"]([^'"]+)['"]/);
if (!pathMatch)
continue;
const importPath = pathMatch[1];
// Handle different import patterns
// Named imports: import { A, B } from './path'
// Also handles: import type { A, B } from './path'
const namedImportsMatch = importText.match(/import(?:\s+type)?\s*{\s*([^}]+)\s*}\s*from/);
if (namedImportsMatch) {
const namedImports = namedImportsMatch[1].split(',');
for (const namedImport of namedImports) {
const trimmed = namedImport.trim();
// Handle aliases: A as B
const aliasMatch = trimmed.match(/([\w]+)\s+as\s+([\w]+)/);
if (aliasMatch) {
const [, original, alias] = aliasMatch;
importMappings.set(alias.trim(), importPath);
logger_1.Logger.debug(`Mapped alias ${alias.trim()} -> ${importPath}`);
}
else {
importMappings.set(trimmed, importPath);
logger_1.Logger.debug(`Mapped named import ${trimmed} -> ${importPath}`);
}
}
}
// Default imports: import A from './path'
const defaultImportMatch = importText.match(/import\s+([\w]+)\s+from/);
if (defaultImportMatch && !namedImportsMatch) {
const defaultImport = defaultImportMatch[1];
importMappings.set(defaultImport, importPath);
logger_1.Logger.debug(`Mapped default import ${defaultImport} -> ${importPath}`);
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract import mappings:', { error });
}
logger_1.Logger.debug(`Final import mappings:`, { importMappings });
return importMappings;
}
extractInterfaces(root) {
const interfaces = [];
try {
// Find all interface declarations
const interfaceNodes = root.findAll({
rule: {
kind: 'interface_declaration'
}
});
logger_1.Logger.debug(`Found ${interfaceNodes.length} interface declarations`);
for (const interfaceNode of interfaceNodes) {
const interfaceText = interfaceNode.text();
logger_1.Logger.debug(`Interface text: ${interfaceText}`);
// Extract interface name using regex
const interfaceMatch = interfaceText.match(/interface\s+([A-Za-z_][A-Za-z0-9_]*)/);
if (interfaceMatch) {
const interfaceName = interfaceMatch[1];
// Check if interface has at least one method
const methodNodes = interfaceNode.findAll({
rule: {
kind: 'method_signature'
}
});
// Consider interface valid if it has at least one method
if (methodNodes.length > 0) {
interfaces.push(interfaceName);
logger_1.Logger.debug(`Found valid interface: ${interfaceName} with ${methodNodes.length} methods `);
}
else {
logger_1.Logger.debug(`Skipping empty interface: ${interfaceName}`);
}
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract interfaces:', { error });
}
logger_1.Logger.debug(`Final interfaces:`, { interfaces });
return interfaces;
}
findAllFunctions(root) {
const functions = [];
try {
// Try multiple approaches to find functions
// Approach 1: Direct function_declaration kind
let functionDeclarations = [];
try {
functionDeclarations = root.findAll({
rule: {
kind: 'function_declaration'
}
});
}
catch (e) {
logger_1.Logger.debug('Could not find function_declaration by kind');
}
// Approach 2: Use pattern matching for exported functions
try {
const exportedFunctions = root.findAll({
rule: {
pattern: 'export function $NAME($$$PARAMS) $$$BODY'
}
});
logger_1.Logger.debug(`Found ${exportedFunctions.length} exported functions via pattern`);
for (const func of exportedFunctions) {
// Check if we already have this function
const funcRange = func.range();
const alreadyExists = functionDeclarations.some((f) => {
const fRange = f.range();
return fRange.start.line === funcRange.start.line;
});
if (!alreadyExists) {
functionDeclarations.push(func);
}
}
}
catch (e) {
logger_1.Logger.debug('Could not find exported functions via pattern');
}
// Approach 3: Use pattern for non-exported functions
try {
const regularFunctions = root.findAll({
rule: {
pattern: 'function $NAME($$$PARAMS) $$$BODY'
}
});
logger_1.Logger.debug(`Found ${regularFunctions.length} regular functions via pattern`);
for (const func of regularFunctions) {
const funcText = func.text();
// Skip if it's already exported (to avoid duplicates)
if (!funcText.includes('export')) {
const funcRange = func.range();
const alreadyExists = functionDeclarations.some((f) => {
const fRange = f.range();
return fRange.start.line === funcRange.start.line;
});
if (!alreadyExists) {
functionDeclarations.push(func);
}
}
}
}
catch (e) {
logger_1.Logger.debug('Could not find regular functions via pattern');
}
// Find variable declarations that might be arrow functions
const variableDeclarations = root.findAll({
rule: {
kind: 'variable_declaration'
}
});
// Filter variable declarations to find arrow functions
const arrowFunctions = [];
for (const varDecl of variableDeclarations) {
const varText = varDecl.text();
// Check if it's an arrow function assignment
if (varText.includes('=>') && (varText.includes('const') || varText.includes('export'))) {
arrowFunctions.push(varDecl);
}
}
logger_1.Logger.debug(`Found ${functionDeclarations.length} function declarations and ${arrowFunctions.length} arrow functions`);
return [...functionDeclarations, ...arrowFunctions];
}
catch (error) {
logger_1.Logger.warn('Warning: Could not find functions:', { error });
return functions;
}
}
extractFunctionName(functionNode) {
try {
// Try to get name from match
let functionName = functionNode.getMatch('NAME')?.text();
if (functionName) {
return functionName;
}
// Try to extract from function declaration
const functionText = functionNode.text();
// First, try to find identifier directly in the function node
const identifier = functionNode.find({
rule: {
kind: 'identifier'
}
});
if (identifier) {
const name = identifier.text();
// Make sure it's the function name, not a parameter
// Function name comes before parameters
const funcMatch = functionText.match(/function\s+([A-Za-z_][A-Za-z0-9_]*)/);
if (funcMatch && funcMatch[1] === name) {
return name;
}
}
// Try regex extraction from function declaration (works for both exported and non-exported)
const functionMatch = functionText.match(/(?:export\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)/);
if (functionMatch) {
return functionMatch[1];
}
// For variable declarations (arrow functions), find the variable name
const varDecl = functionNode.find({
rule: {
kind: 'variable_declaration'
}
});
if (varDecl) {
const declarators = varDecl.findAll({
rule: {
kind: 'variable_declarator'
}
});
if (declarators.length > 0) {
const varIdentifier = declarators[0].find({
rule: {
kind: 'identifier'
}
});
if (varIdentifier) {
return varIdentifier.text();
}
}
}
// Try to extract from arrow function using regex
const arrowMatch = functionText.match(/(?:export\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=/);
if (arrowMatch) {
return arrowMatch[1];
}
return undefined;
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract function name:', { error });
return undefined;
}
}
extractFunctionParameters(functionNode) {
const parameters = [];
try {
// Find formal parameters
const parameterNodes = functionNode.findAll({
rule: {
kind: 'formal_parameters'
}
});
if (parameterNodes.length === 0) {
return parameters;
}
const formalParams = parameterNodes[0];
// Find individual parameters
const requiredParamNodes = formalParams.findAll({
rule: {
kind: 'required_parameter'
}
});
const optionalParamNodes = formalParams.findAll({
rule: {
kind: 'optional_parameter'
}
});
const paramNodes = [...requiredParamNodes, ...optionalParamNodes];
for (const paramNode of paramNodes) {
const paramText = paramNode.text();
logger_1.Logger.debug('Function parameter text:', paramText);
// Try to parse object type parameter FIRST: name: { prop1: Type1, prop2: Type2 }
// This must come before simple parameter parsing to avoid matching nested properties
const objectParamMatch = paramText.match(/(\w+)(\?)?\s*:\s*\{/);
if (objectParamMatch) {
const [, name, optional] = objectParamMatch;
// For object types, extract the full object type string
// Use a more robust regex that handles multi-line and nested braces
const fullTypeMatch = paramText.match(/:\s*(\{[^}]*\})/s);
const type = fullTypeMatch ? fullTypeMatch[1] : 'object';
parameters.push({
name: name.trim(),
type: type.trim(),
isOptional: !!optional,
});
continue;
}
// Try to parse simple parameter: name: type (handles typeof and ReturnType<typeof ...>)
const simpleParamMatch = paramText.match(/(\w+)(\?)?\s*:\s*(?:ReturnType\s*<\s*typeof\s+)?(?:typeof\s+)?(\w+)/);
if (simpleParamMatch) {
const [, name, optional, type] = simpleParamMatch;
parameters.push({
name: name.trim(),
type: type.trim(),
isOptional: !!optional,
});
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract function parameters:', { error });
}
return parameters;
}
extractObjectTypeProperties(functionNode, paramName) {
const properties = [];
try {
const functionText = functionNode.text();
// Find the parameter with the given name - handle both single line and multi-line
// Pattern: paramName: { prop1: Type1, prop2: Type2 }
const paramPattern = new RegExp(`${paramName}\\s*:\\s*\\{([^}]+)\\}`, 's');
const match = functionText.match(paramPattern);
if (!match || !match[1]) {
return properties;
}
const objectBody = match[1];
// Extract properties: prop1: Type1, prop2: Type2
// Handle both single line and multi-line formats
// Match property name and type (type can be a simple identifier like IUserRepository)
const propertyPattern = /(\w+)\s*:\s*(\w+)/g;
let propMatch;
while ((propMatch = propertyPattern.exec(objectBody)) !== null) {
if (propMatch[1] && propMatch[2]) {
properties.push({
name: propMatch[1].trim(),
type: propMatch[2].trim(),
});
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract object type properties:', { error });
}
return properties;
}
extractFunctionReturnType(functionNode) {
try {
const functionText = functionNode.text();
// Extract return type annotation after the closing ) and before { or =>
// This correctly captures the function return type, not parameter types
const returnTypeMatch = functionText.match(/\)\s*:\s*([A-Za-z_][A-Za-z0-9_<>[\],.\s]*?)\s*(?:\{|=>)/);
if (returnTypeMatch && returnTypeMatch[1]) {
return returnTypeMatch[1].trim();
}
return undefined;
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract function return type:', { error });
return undefined;
}
}
isExportedFunction(functionNode) {
try {
const functionText = functionNode.text();
// Check if the function text itself includes export
if (functionText.includes('export')) {
return true;
}
// Check parent nodes - exported functions might be wrapped
let parent = functionNode.parent();
let depth = 0;
while (parent && depth < 5) {
const parentText = parent.text();
if (parentText.includes('export')) {
return true;
}
parent = parent.parent();
depth++;
}
return false;
}
catch (error) {
return false;
}
}
findAllExportedValues(root) {
const values = [];
try {
// Find variable declarations - try multiple AST node kinds
let variableDeclarations = [];
// Try lexical_declaration (for const/let)
try {
const lexicalDecls = root.findAll({
rule: {
kind: 'lexical_declaration'
}
});
variableDeclarations.push(...lexicalDecls);
}
catch (e) {
logger_1.Logger.debug('Could not find lexical_declaration nodes');
}
// Try variable_declaration as fallback
try {
const varDecls = root.findAll({
rule: {
kind: 'variable_declaration'
}
});
variableDeclarations.push(...varDecls);
}
catch (e) {
logger_1.Logger.debug('Could not find variable_declaration nodes');
}
logger_1.Logger.debug(`Found ${variableDeclarations.length} variable declarations`);
for (const varDecl of variableDeclarations) {
const varText = varDecl.text();
// Check if it's a const declaration first
if (!varText.includes('const ')) {
continue;
}
// Check if it's exported - check the node itself and parent nodes
const isExported = this.isExportedValue(varDecl);
if (isExported) {
values.push(varDecl);
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not find exported values:', { error });
}
logger_1.Logger.debug(`Returning ${values.length} exported values`);
return values;
}
extractValueName(valueNode) {
try {
// Find the variable declarator
const declarators = valueNode.findAll({
rule: {
kind: 'variable_declarator'
}
});
if (declarators.length === 0) {
return undefined;
}
const declarator = declarators[0];
const identifier = declarator.find({
rule: {
kind: 'identifier'
}
});
if (identifier) {
return identifier.text();
}
// Fallback: try to extract from text
const text = declarator.text();
const match = text.match(/^(\w+)\s*:/);
return match ? match[1] : undefined;
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract value name:', { error });
return undefined;
}
}
extractValueType(valueNode) {
try {
const declarators = valueNode.findAll({
rule: {
kind: 'variable_declarator'
}
});
if (declarators.length === 0) {
return undefined;
}
const declarator = declarators[0];
// Find type annotation
const typeAnnotation = declarator.find({
rule: {
kind: 'type_annotation'
}
});
if (typeAnnotation) {
// Find the type identifier
const typeIdentifier = typeAnnotation.find({
rule: {
kind: 'type_identifier'
}
});
if (typeIdentifier) {
return typeIdentifier.text();
}
// Try to extract from text
const typeText = typeAnnotation.text();
const match = typeText.match(/:\s*(\w+)/);
return match ? match[1] : undefined;
}
return undefined;
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract value type:', { error });
return undefined;
}
}
isExportedValue(valueNode) {
try {
// Check if the variable declaration itself has export
const varText = valueNode.text();
if (varText.includes('export ')) {
return true;
}
// Check parent nodes for export
let parent = valueNode.parent();
let depth = 0;
while (parent && depth < 5) {
try {
const parentText = parent.text();
if (parentText.includes('export ')) {
return true;
}
parent = parent.parent();
depth++;
}
catch (e) {
break;
}
}
return false;
}
catch (error) {
return false;
}
}
/**
* Finds names that are re-exported from external (non-relative) packages.
* Handles: export type { IFoo, IBar } from 'some-package'
* export { IFoo } from 'some-package'
* These names should be treated as valid DI tokens even though they have no
* local declaration.
*/
extractExternalReExports(root) {
const names = [];
try {
const exportStatements = root.findAll({
rule: { kind: 'export_statement' }
});
for (const exportNode of exportStatements) {
const text = exportNode.text();
// Only care about re-exports (must have a `from` clause)
const fromMatch = text.match(/from\s+['"]([^'"]+)['"]/);
if (!fromMatch)
continue;
const fromPath = fromMatch[1];
// Skip local re-exports (they will be resolved by following the local file)
if (fromPath.startsWith('.'))
continue;
// Extract named exports: export [type] { X, Y as Z } from ...
const namedMatch = text.match(/export\s+(?:type\s+)?\{\s*([^}]+)\s*\}/);
if (!namedMatch)
continue;
const parts = namedMatch[1].split(',');
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed)
continue;
// 'OriginalName as LocalName' → use LocalName (the exported symbol)
const aliasMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
names.push(aliasMatch ? aliasMatch[2] : trimmed);
}
}
}
catch (error) {
logger_1.Logger.warn('Warning: Could not extract external re-exports:', { error });
}
return names;
}
}
exports.ASTParser = ASTParser;
//# sourceMappingURL=ast-parser.js.map