UNPKG

@notjustcoders/ioc-arise

Version:

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

820 lines 35.1 kB
"use strict"; 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