// ============================================================================ // AI Testing Suite - Code Parser Utility // Uses ts-morph for deep TypeScript/JavaScript analysis // ============================================================================ import { ModuleInfo, FunctionInfo, ClassInfo, ImportInfo, ExportInfo, InterfaceInfo, VariableInfo, ParameterInfo, PropertyInfo, ApiEndpoint, DatabaseOperation, ErrorPattern, CodePattern, } from '../types'; import { readFileContent } from './file-utils'; /** * Parse a TypeScript/JavaScript file and extract detailed information. * Uses regex-based parsing for zero-dependency operation. * Falls back gracefully if file cannot be parsed. */ export function parseModule(filePath: string): ModuleInfo { const content = readFileContent(filePath); if (!content) { return emptyModule(filePath); } return { filePath, imports: parseImports(content), exports: parseExports(content), functions: parseFunctions(content, filePath), classes: parseClasses(content, filePath), interfaces: parseInterfaces(content, filePath), variables: parseVariables(content), dependencies: extractDependencies(content), hasDefaultExport: /export\s+default\s/.test(content), }; } function emptyModule(filePath: string): ModuleInfo { return { filePath, imports: [], exports: [], functions: [], classes: [], interfaces: [], variables: [], dependencies: [], hasDefaultExport: false, }; } function parseImports(content: string): ImportInfo[] { const imports: ImportInfo[] = []; const importRegex = /import\s+(?:(?:(\w+)\s*,?\s*)?(?:\{([^}]*)\}\s*)?(?:\*\s+as\s+(\w+)\s*)?from\s+)?['"]([^'"]+)['"]/g; let match: RegExpExecArray | null; while ((match = importRegex.exec(content)) !== null) { const defaultImport = match[1]; const namedImports = match[2]; const namespaceImport = match[3]; const moduleName = match[4]; const importedNames: string[] = []; if (defaultImport) importedNames.push(defaultImport); if (namedImports) { importedNames.push(...namedImports.split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean)); } if (namespaceImport) importedNames.push(namespaceImport); imports.push({ moduleName, importedNames, isDefault: !!defaultImport && !namedImports, isNamespace: !!namespaceImport, isExternal: !moduleName.startsWith('.') && !moduleName.startsWith('/'), }); } // Also check require() statements const requireRegex = /(?:const|let|var)\s+(?:(\w+)|(\{[^}]*\}))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((match = requireRegex.exec(content)) !== null) { const defaultName = match[1]; const destructured = match[2]; const moduleName = match[3]; const importedNames: string[] = []; if (defaultName) importedNames.push(defaultName); if (destructured) { importedNames.push(...destructured.replace(/[{}]/g, '').split(',').map(n => n.trim()).filter(Boolean)); } imports.push({ moduleName, importedNames, isDefault: !!defaultName, isNamespace: false, isExternal: !moduleName.startsWith('.') && !moduleName.startsWith('/'), }); } return imports; } function parseExports(content: string): ExportInfo[] { const exports: ExportInfo[] = []; // export function/const/class/interface/type/enum const exportRegex = /export\s+(default\s+)?(async\s+)?(function|const|let|var|class|interface|type|enum)\s+(\w+)/g; let match: RegExpExecArray | null; while ((match = exportRegex.exec(content)) !== null) { const isDefault = !!match[1]; const kind = match[3]; const name = match[4]; let type: ExportInfo['type'] = 'variable'; if (kind === 'function') type = 'function'; else if (kind === 'class') type = 'class'; else if (kind === 'interface') type = 'interface'; else if (kind === 'type') type = 'type'; else if (kind === 'enum') type = 'enum'; exports.push({ name, type, isDefault }); } // module.exports if (/module\.exports\s*=/.test(content)) { exports.push({ name: 'default', type: 'variable', isDefault: true }); } return exports; } function parseFunctions(content: string, filePath: string): FunctionInfo[] { const functions: FunctionInfo[] = []; const lines = content.split('\n'); // Match function declarations and arrow functions const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^\s{]+))?\s*\{/g; const arrowRegex = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::[^=]*)?\s*=\s*(?:async\s+)?\(([^)]*)\)(?:\s*:\s*([^\s=]+))?\s*=>/g; let match: RegExpExecArray | null; while ((match = funcRegex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; functions.push({ name: match[1], filePath, line, params: parseParams(match[2]), returnType: match[3] || 'void', isAsync: content.substring(Math.max(0, match.index - 10), match.index + match[0].length).includes('async'), isExported: match[0].startsWith('export'), complexity: calculateComplexity(extractFunctionBody(content, match.index)), dependencies: [], }); } while ((match = arrowRegex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; functions.push({ name: match[1], filePath, line, params: parseParams(match[2]), returnType: match[3] || 'unknown', isAsync: match[0].includes('async'), isExported: match[0].startsWith('export'), complexity: calculateComplexity(extractFunctionBody(content, match.index)), dependencies: [], }); } // Class methods are handled separately in parseClasses return functions; } function parseParams(paramStr: string): ParameterInfo[] { if (!paramStr.trim()) return []; return paramStr.split(',').map(p => { const trimmed = p.trim(); const optional = trimmed.includes('?'); const parts = trimmed.replace('?', '').split(':').map(s => s.trim()); const hasDefault = parts[0].includes('='); const namePart = parts[0].split('=')[0].trim(); return { name: namePart, type: parts[1] || 'any', optional: optional || hasDefault, defaultValue: hasDefault ? parts[0].split('=')[1]?.trim() : undefined, }; }).filter(p => p.name); } function parseClasses(content: string, filePath: string): ClassInfo[] { const classes: ClassInfo[] = []; const classRegex = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?\s*\{/g; let match: RegExpExecArray | null; while ((match = classRegex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; const className = match[1]; const classBody = extractFunctionBody(content, match.index); classes.push({ name: className, filePath, line, methods: parseClassMethods(classBody, filePath, className), properties: parseClassProperties(classBody), isExported: match[0].startsWith('export'), extends: match[2] || undefined, implements: match[3] ? match[3].split(',').map(s => s.trim()) : undefined, dependencies: [], }); } return classes; } function parseClassMethods(classBody: string, filePath: string, className: string): FunctionInfo[] { const methods: FunctionInfo[] = []; const methodRegex = /(?:(public|private|protected)\s+)?(?:(static)\s+)?(?:(async)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^\s{]+))?\s*\{/g; let match: RegExpExecArray | null; while ((match = methodRegex.exec(classBody)) !== null) { if (['if', 'for', 'while', 'switch', 'catch'].includes(match[4])) continue; methods.push({ name: match[4], filePath, line: 0, params: parseParams(match[5]), returnType: match[6] || 'void', isAsync: !!match[3], isExported: true, complexity: calculateComplexity(extractFunctionBody(classBody, match.index)), dependencies: [], }); } return methods; } function parseClassProperties(classBody: string): PropertyInfo[] { const properties: PropertyInfo[] = []; const propRegex = /(?:(public|private|protected)\s+)?(?:(static)\s+)?(?:(readonly)\s+)?(\w+)(?:\?)?(?:\s*:\s*([^;=]+))?(?:\s*=\s*([^;]+))?;/g; let match: RegExpExecArray | null; while ((match = propRegex.exec(classBody)) !== null) { properties.push({ name: match[4], type: match[5]?.trim() || 'any', visibility: (match[1] as 'public' | 'private' | 'protected') || 'public', isStatic: !!match[2], isReadonly: !!match[3], }); } return properties; } function parseInterfaces(content: string, filePath: string): InterfaceInfo[] { const interfaces: InterfaceInfo[] = []; const ifaceRegex = /(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+[^{]+)?\s*\{/g; let match: RegExpExecArray | null; while ((match = ifaceRegex.exec(content)) !== null) { const body = extractFunctionBody(content, match.index); interfaces.push({ name: match[1], filePath, properties: parseInterfaceProperties(body), isExported: match[0].startsWith('export'), }); } return interfaces; } function parseInterfaceProperties(body: string): PropertyInfo[] { const properties: PropertyInfo[] = []; const propRegex = /(?:(readonly)\s+)?(\w+)(\?)?\s*:\s*([^;}\n]+)/g; let match: RegExpExecArray | null; while ((match = propRegex.exec(body)) !== null) { properties.push({ name: match[2], type: match[4].trim(), visibility: 'public', isStatic: false, isReadonly: !!match[1], }); } return properties; } function parseVariables(content: string): VariableInfo[] { const variables: VariableInfo[] = []; const varRegex = /(?:export\s+)?(const|let|var)\s+(\w+)(?:\s*:\s*([^=;]+))?\s*=\s*([^;\n]+)/g; let match: RegExpExecArray | null; while ((match = varRegex.exec(content)) !== null) { if (match[4].includes('=>') || match[4].includes('function')) continue; variables.push({ name: match[2], type: match[3]?.trim() || 'inferred', isConst: match[1] === 'const', isExported: content.substring(Math.max(0, match.index - 7), match.index + 7).includes('export'), value: match[4].trim().substring(0, 100), }); } return variables; } function extractDependencies(content: string): string[] { const deps: string[] = []; const importRegex = /(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g; let match: RegExpExecArray | null; while ((match = importRegex.exec(content)) !== null) { deps.push(match[1]); } return [...new Set(deps)]; } function calculateComplexity(body: string): number { if (!body) return 1; let complexity = 1; const patterns = [/\bif\b/g, /\belse\s+if\b/g, /\bfor\b/g, /\bwhile\b/g, /\bswitch\b/g, /\bcase\b/g, /\bcatch\b/g, /\?\?/g, /\?\./g, /&&/g, /\|\|/g, /\?\s*[^:]+\s*:/g]; for (const pattern of patterns) { const matches = body.match(pattern); if (matches) complexity += matches.length; } return complexity; } function extractFunctionBody(content: string, startIndex: number): string { let braceCount = 0; let started = false; let bodyStart = startIndex; for (let i = startIndex; i < content.length; i++) { if (content[i] === '{') { if (!started) bodyStart = i; braceCount++; started = true; } else if (content[i] === '}') { braceCount--; if (started && braceCount === 0) { return content.substring(bodyStart, i + 1); } } } return content.substring(bodyStart, Math.min(bodyStart + 500, content.length)); } // --- API Endpoint Detection --- export function detectApiEndpoints(content: string, filePath: string): ApiEndpoint[] { const endpoints: ApiEndpoint[] = []; const methods = ['get', 'post', 'put', 'delete', 'patch', 'options']; for (const method of methods) { // Express-style: app.get('/path', handler) or router.get('/path', handler) const regex = new RegExp(`(?:app|router|server)\\.${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, 'g'); let match: RegExpExecArray | null; while ((match = regex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; endpoints.push({ method: method.toUpperCase() as ApiEndpoint['method'], path: match[1], handler: `${method}_handler_line_${line}`, filePath, middleware: detectMiddleware(content, match.index), params: detectRouteParams(match[1]), authentication: detectAuth(content, match.index), }); } // Decorator-style: @Get('/path'), @Post('/path') const decoratorRegex = new RegExp(`@${method.charAt(0).toUpperCase() + method.slice(1)}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]\\s*\\)`, 'g'); while ((match = decoratorRegex.exec(content)) !== null) { endpoints.push({ method: method.toUpperCase() as ApiEndpoint['method'], path: match[1], handler: `decorated_handler`, filePath, middleware: [], params: detectRouteParams(match[1]), authentication: detectAuth(content, match.index), }); } } return endpoints; } function detectMiddleware(content: string, position: number): string[] { const middleware: string[] = []; const lineStart = content.lastIndexOf('\n', position); const lineEnd = content.indexOf('\n', position); const line = content.substring(lineStart, lineEnd); const middlewarePatterns = ['auth', 'validate', 'cors', 'rateLimit', 'csrf', 'sanitize', 'upload']; for (const pattern of middlewarePatterns) { if (line.toLowerCase().includes(pattern.toLowerCase())) { middleware.push(pattern); } } return middleware; } function detectRouteParams(path: string): ParameterInfo[] { const params: ParameterInfo[] = []; const paramRegex = /:(\w+)/g; let match: RegExpExecArray | null; while ((match = paramRegex.exec(path)) !== null) { params.push({ name: match[1], type: 'string', optional: false }); } return params; } function detectAuth(content: string, position: number): boolean { const surrounding = content.substring(Math.max(0, position - 200), position + 200); return /auth|guard|protect|jwt|bearer|session|token|passport/i.test(surrounding); } // --- Database Operation Detection --- export function detectDatabaseOperations(content: string, filePath: string): DatabaseOperation[] { const operations: DatabaseOperation[] = []; const patterns: { regex: RegExp; type: DatabaseOperation['type']; raw: boolean }[] = [ { regex: /\.find(?:One|Many|All|By)?\s*\(/g, type: 'query', raw: false }, { regex: /\.select\s*\(/g, type: 'query', raw: false }, { regex: /\.where\s*\(/g, type: 'query', raw: false }, { regex: /\.create\s*\(/g, type: 'insert', raw: false }, { regex: /\.insertOne|insertMany\s*\(/g, type: 'insert', raw: false }, { regex: /\.update(?:One|Many)?\s*\(/g, type: 'update', raw: false }, { regex: /\.save\s*\(/g, type: 'update', raw: false }, { regex: /\.delete(?:One|Many)?\s*\(/g, type: 'delete', raw: false }, { regex: /\.remove\s*\(/g, type: 'delete', raw: false }, { regex: /\.destroy\s*\(/g, type: 'delete', raw: false }, { regex: /\.transaction\s*\(/g, type: 'transaction', raw: false }, { regex: /\.query\s*\(\s*['"`]/g, type: 'query', raw: true }, { regex: /\.raw\s*\(\s*['"`]/g, type: 'query', raw: true }, { regex: /\.exec(?:ute)?\s*\(\s*['"`]/g, type: 'query', raw: true }, ]; for (const { regex, type, raw } of patterns) { let match: RegExpExecArray | null; while ((match = regex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; operations.push({ type, model: 'detected', filePath, line, raw, }); } } return operations; } // --- Error Pattern Detection --- export function detectErrorPatterns(content: string, filePath: string): ErrorPattern[] { const patterns: ErrorPattern[] = []; // try-catch blocks const tryCatchRegex = /try\s*\{/g; let match: RegExpExecArray | null; while ((match = tryCatchRegex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; const surrounding = content.substring(match.index, Math.min(match.index + 500, content.length)); patterns.push({ type: 'try-catch', filePath, line, handlesSpecificErrors: /instanceof\s+\w+Error|\.name\s*===|\.code\s*===/.test(surrounding), rethrows: /throw\s/.test(surrounding.substring(surrounding.indexOf('catch'))), }); } // .catch() on promises const promiseCatchRegex = /\.catch\s*\(/g; while ((match = promiseCatchRegex.exec(content)) !== null) { const line = content.substring(0, match.index).split('\n').length; patterns.push({ type: 'promise-catch', filePath, line, handlesSpecificErrors: false, rethrows: false, }); } // Error middleware (Express-style) if (/\(\s*err\s*,\s*req\s*,\s*res\s*,\s*next\s*\)/.test(content)) { patterns.push({ type: 'error-middleware', filePath, line: 0, handlesSpecificErrors: true, rethrows: false, }); } // Global error handlers if (/process\.on\s*\(\s*['"]uncaughtException|unhandledRejection['"]/.test(content)) { patterns.push({ type: 'global-handler', filePath, line: 0, handlesSpecificErrors: false, rethrows: false, }); } return patterns; } // --- Parameter Usage Analysis --- // Analyzes function body to detect property/method accesses on each parameter. // This enables the Writer to generate intelligent mock objects. export interface ParamPropertyAccess { name: string; isMethod: boolean; // true if called as method, e.g. param.foo() valueHint?: string; // inferred value hint: 'string' | 'array' | 'object' | 'function' | 'number' | 'boolean' subProperties?: string[]; // nested accesses, e.g. param.foo.bar → subProperties: ['bar'] } export interface ParameterUsageInfo { paramName: string; properties: ParamPropertyAccess[]; isIterable: boolean; // used in for..of, .map(), .forEach(), .filter(), etc. isDestructured: boolean; destructuredNames: string[]; isSpread: boolean; // ...param } /** * Analyze which properties/methods are accessed on each parameter within a function body. * Used by the Writer to generate test values with the correct shape. */ export function analyzeParameterUsage(body: string, paramNames: string[]): ParameterUsageInfo[] { const results: ParameterUsageInfo[] = []; for (const paramName of paramNames) { const info: ParameterUsageInfo = { paramName, properties: [], isIterable: false, isDestructured: false, destructuredNames: [], isSpread: false, }; // Skip very short/common param names that cause false positives if (paramName.length < 2) { results.push(info); continue; } const escaped = paramName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 1. Detect property accesses: param.property or param.method() or param.method() const propAccessRegex = new RegExp(`${escaped}\\.(\\w+)(?:\\s*(?:<[^>]*>\\s*)?\\()?`, 'g'); const seenProps = new Set(); let match: RegExpExecArray | null; while ((match = propAccessRegex.exec(body)) !== null) { const propName = match[1]; if (seenProps.has(propName)) continue; seenProps.add(propName); const isMethod = match[0].endsWith('(') || /\w+\s*<[^>]*>\s*\($/.test(match[0]); // Try to infer value type from usage context let valueHint: string | undefined; if (isMethod) { // Known string methods if (['trim', 'toLowerCase', 'toUpperCase', 'split', 'replace', 'slice', 'substring', 'startsWith', 'endsWith', 'includes', 'match', 'search', 'padStart', 'padEnd', 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf', 'repeat', 'normalize', 'trimStart', 'trimEnd'].includes(propName)) { valueHint = 'string'; } // Known array methods else if (['map', 'filter', 'forEach', 'reduce', 'find', 'findIndex', 'some', 'every', 'flat', 'flatMap', 'sort', 'reverse', 'splice', 'slice', 'push', 'pop', 'shift', 'unshift', 'concat', 'join', 'entries', 'keys', 'values'].includes(propName)) { valueHint = 'array'; } } else { // Property access — check if .length is used (string or array) if (propName === 'length') { // Check broader context for type hints const ctx = body; if (new RegExp(`${escaped}\\.map\\s*\\(`).test(ctx) || new RegExp(`${escaped}\\.forEach\\s*\\(`).test(ctx) || new RegExp(`${escaped}\\.filter\\s*\\(`).test(ctx) || new RegExp(`for\\s*\\([^)]*of\\s+${escaped}`).test(ctx)) { valueHint = 'array'; } else { valueHint = 'string'; // default .length to string } } } // Detect sub-property accesses: param.prop.subProp or param.prop.method() const subProperties: string[] = []; const subRegex = new RegExp(`${escaped}\\.${propName}\\.(\\w+)(\\s*\\()?`, 'g'); let subMatch: RegExpExecArray | null; const STRING_METHODS = new Set(['trim', 'toLowerCase', 'toUpperCase', 'split', 'replace', 'slice', 'substring', 'startsWith', 'endsWith', 'includes', 'match', 'search', 'padStart', 'padEnd', 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf', 'repeat', 'normalize', 'trimStart', 'trimEnd']); const ARRAY_METHODS = new Set(['map', 'filter', 'forEach', 'reduce', 'find', 'findIndex', 'some', 'every', 'flat', 'flatMap', 'sort', 'reverse', 'splice', 'push', 'pop', 'shift', 'unshift', 'concat', 'join', 'entries', 'keys', 'values']); while ((subMatch = subRegex.exec(body)) !== null) { const subName = subMatch[1]; const isSubMethod = !!subMatch[2]; // Infer parent property type from sub-property/method usage if (!valueHint) { if (isSubMethod && STRING_METHODS.has(subName)) { valueHint = 'string'; } else if (isSubMethod && ARRAY_METHODS.has(subName)) { valueHint = 'array'; } else if (subName === 'length') { // .length could be string or array — check for array patterns if (new RegExp(`${escaped}\\.${propName}\\.(map|forEach|filter|reduce|find|some|every)\\s*\\(`).test(body) || new RegExp(`for\\s*\\([^)]*of\\s+${escaped}\\.${propName}`).test(body)) { valueHint = 'array'; } else { valueHint = 'array'; // default .prop.length to array } } } if (!subProperties.includes(subName)) { subProperties.push(subName); } } // For sub-properties, infer their types from third-level accesses // e.g. param.core.identity.trim() → identity is a string const enrichedSubProperties: string[] = []; if (!valueHint && subProperties.length > 0) { for (const sub of subProperties) { const subEsc = sub.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let subHint: string | undefined; // Check third-level method calls: param.prop.sub.method() const thirdRegex = new RegExp(`${escaped}\\.${propName}\\.${subEsc}\\.(\\w+)\\s*\\(`, 'g'); let tm: RegExpExecArray | null; while ((tm = thirdRegex.exec(body)) !== null) { if (STRING_METHODS.has(tm[1])) { subHint = 'string'; break; } if (ARRAY_METHODS.has(tm[1])) { subHint = 'array'; break; } } // Check third-level .length if (!subHint && new RegExp(`${escaped}\\.${propName}\\.${subEsc}\\.length\\b`).test(body)) { subHint = 'array'; } // Check iteration on sub-property if (!subHint && ( new RegExp(`for\\s*\\([^)]*of\\s+${escaped}\\.${propName}\\.${subEsc}(?!\\.)\\b`).test(body) || new RegExp(`${escaped}\\.${propName}\\.${subEsc}\\.(map|forEach|filter|reduce|find|some|every)\\s*\\(`).test(body) )) { subHint = 'array'; } enrichedSubProperties.push(subHint ? `${sub}:${subHint}` : sub); } } // Detect spread/iteration on property: [...param.prop] or for(x of param.prop) if (!valueHint && !isMethod) { if (new RegExp(`\\.\\.\\.${escaped}\\.${propName}\\b`).test(body) || new RegExp(`for\\s*\\([^)]*of\\s+${escaped}\\.${propName}\\b`).test(body) || new RegExp(`for\\s*\\([^)]*in\\s+${escaped}\\.${propName}\\b`).test(body)) { valueHint = 'array'; } } // Only keep subProperties if no type was inferred (otherwise the parent IS the type) info.properties.push({ name: propName, isMethod, valueHint, subProperties: (!valueHint && enrichedSubProperties.length > 0) ? enrichedSubProperties : (!valueHint && subProperties.length > 0) ? subProperties : undefined, }); } // 2. Detect destructuring: const { a, b } = param const destructureRegex = new RegExp(`(?:const|let|var)\\s*\\{([^}]+)\\}\\s*=\\s*${escaped}`, 'g'); while ((match = destructureRegex.exec(body)) !== null) { info.isDestructured = true; const names = match[1].split(',').map(n => n.trim().split(':')[0].split('=')[0].trim()).filter(Boolean); for (const name of names) { if (!info.destructuredNames.includes(name)) { info.destructuredNames.push(name); } } } // 2b. Analyze usage of destructured variables to infer their types // This turns e.g. `const { emails } = args; emails.length` into // a property entry for `emails` with valueHint 'array' if (info.isDestructured) { const STRING_METHODS_SET = new Set(['trim', 'toLowerCase', 'toUpperCase', 'split', 'replace', 'slice', 'substring', 'startsWith', 'endsWith', 'includes', 'match', 'search', 'padStart', 'padEnd', 'charAt', 'indexOf', 'lastIndexOf', 'repeat', 'trimStart', 'trimEnd']); const ARRAY_METHODS_SET = new Set(['map', 'filter', 'forEach', 'reduce', 'find', 'findIndex', 'some', 'every', 'flat', 'flatMap', 'sort', 'reverse', 'splice', 'push', 'pop', 'shift', 'unshift', 'concat', 'join', 'entries', 'keys', 'values']); for (const dName of info.destructuredNames) { if (dName.length < 2) continue; const dEscaped = dName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Skip if already detected as a direct property if (info.properties.some(p => p.name === dName)) continue; let dValueHint: string | undefined; // Check for method calls: destructuredVar.method() const dMethodRegex = new RegExp(`${dEscaped}\\.(\\w+)\\s*\\(`, 'g'); let dMatch: RegExpExecArray | null; while ((dMatch = dMethodRegex.exec(body)) !== null) { const methodName = dMatch[1]; if (STRING_METHODS_SET.has(methodName)) { dValueHint = 'string'; break; } if (ARRAY_METHODS_SET.has(methodName)) { dValueHint = 'array'; break; } } // Check for .length access if (!dValueHint && new RegExp(`${dEscaped}\\.length`).test(body)) { // Check if it's iterable too if (new RegExp(`${dEscaped}\\.(map|forEach|filter|reduce|find|some|every)\\s*\\(`).test(body) || new RegExp(`for\\s*\\([^)]*of\\s+${dEscaped}`).test(body)) { dValueHint = 'array'; } else { dValueHint = 'array'; // default .length on destructured to array } } // Check for iteration if (!dValueHint && ( new RegExp(`for\\s*\\([^)]*of\\s+${dEscaped}`).test(body) || new RegExp(`\\.\\.\\.${dEscaped}`).test(body) )) { dValueHint = 'array'; } // Check for property access: destructuredVar.prop (not a known method) if (!dValueHint) { const dPropRegex = new RegExp(`${dEscaped}\\.(\\w+)`, 'g'); const dProps: string[] = []; let dpMatch: RegExpExecArray | null; while ((dpMatch = dPropRegex.exec(body)) !== null) { if (!dProps.includes(dpMatch[1])) dProps.push(dpMatch[1]); } if (dProps.length > 0) { // Has property accesses — capture sub-properties with type inference // For each sub-prop, check third-level accesses to infer its type const enrichedSubProps: string[] = []; for (const sub of dProps) { const subEsc = sub.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let subHint: string | undefined; // Check for method calls: dName.sub.method() const thirdRegex = new RegExp(`${dEscaped}\\.${subEsc}\\.(\\w+)\\s*\\(`, 'g'); let tm: RegExpExecArray | null; while ((tm = thirdRegex.exec(body)) !== null) { if (STRING_METHODS_SET.has(tm[1])) { subHint = 'string'; break; } if (ARRAY_METHODS_SET.has(tm[1])) { subHint = 'array'; break; } } // Check for .length at third level if (!subHint && new RegExp(`${dEscaped}\\.${subEsc}\\.length\\b`).test(body)) { subHint = 'array'; } // Check for iteration: for...of dName.sub if (!subHint && ( new RegExp(`for\\s*\\([^)]*of\\s+${dEscaped}\\.${subEsc}(?!\\.)\\b`).test(body) || new RegExp(`${dEscaped}\\.${subEsc}\\.(map|forEach|filter|reduce|find|some|every)\\s*\\(`).test(body) )) { subHint = 'array'; } // Store with hint annotation: "sub:string" or just "sub" enrichedSubProps.push(subHint ? `${sub}:${subHint}` : sub); } info.properties.push({ name: dName, isMethod: false, valueHint: undefined, // no overall type — use subProperties subProperties: enrichedSubProps, }); continue; // skip the default push below } } // Add as a property entry with inferred type info.properties.push({ name: dName, isMethod: false, valueHint: dValueHint, }); } } // 3. Detect if parameter ITSELF is used as iterable (not param.prop) if (new RegExp(`for\\s*\\([^)]*of\\s+${escaped}(?!\\.)\\b`).test(body) || new RegExp(`${escaped}\\.(map|forEach|filter|reduce|find|some|every|flatMap|entries)\\s*\\(`).test(body) || new RegExp(`\\.\\.\\.${escaped}(?!\\.)\\b`).test(body)) { info.isIterable = true; } // 3b. Detect property-level iterable: for...of param.prop or param.prop.map() for (const prop of info.properties) { if (prop.valueHint) continue; // already inferred const propEsc = prop.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (new RegExp(`for\\s*\\([^)]*of\\s+${escaped}\\.${propEsc}(?!\\.)\\b`).test(body) || new RegExp(`${escaped}\\.${propEsc}\\.(map|forEach|filter|reduce|find|some|every|flatMap)\\s*\\(`).test(body)) { prop.valueHint = 'array'; } } // 4. Detect spread usage if (new RegExp(`\\.\\.\\.${escaped}(?!\\.)\\b`).test(body)) { info.isSpread = true; } // 5. Detect indexed access: param[0], param[key] — hints at array/map if (new RegExp(`${escaped}\\[`).test(body)) { if (!info.isIterable) { info.isIterable = true; // likely array or map } } results.push(info); } return results; } /** * Extract the body of a named function from source content. * Returns the body string or empty string if not found. */ export function extractNamedFunctionBody(content: string, funcName: string): string { // Match function declaration: export async function name(...) { or function name(...) const funcDeclRegex = new RegExp( `(?:export\\s+)?(?:async\\s+)?function\\s+${funcName}\\s*\\([^)]*\\)[^{]*\\{` ); let match = funcDeclRegex.exec(content); if (!match) { // Match arrow function or const assignment with optional type annotation: // export const name: Type = async (...) => { // const name = async (...) => { // const name = (...) => { const arrowRegex = new RegExp( `(?:export\\s+)?(?:const|let|var)\\s+${funcName}\\s*(?::[^=]*)?=\\s*(?:async\\s+)?\\([^)]*\\)\\s*(?::[^=]*)?=>\\s*(?:\\{|[^{])` ); match = arrowRegex.exec(content); } if (!match) { // Match function expression: const name = function(...) { const funcExprRegex = new RegExp( `(?:export\\s+)?(?:const|let|var)\\s+${funcName}\\s*(?::[^=]*)?=\\s*(?:async\\s+)?function\\s*\\([^)]*\\)[^{]*\\{` ); match = funcExprRegex.exec(content); } if (!match) { // Fallback: search for the function name followed by = ... { or function name( // This catches class methods, object methods, etc. const fallbackRegex = new RegExp( `${funcName}\\s*(?:=\\s*(?:async\\s+)?)?(?:\\([^)]*\\)|[^{]*)\\{` ); match = fallbackRegex.exec(content); } if (!match) return ''; return extractFunctionBody(content, match.index); } // --- Code Pattern Detection --- export function detectCodePatterns(content: string, filePath: string): CodePattern[] { const patterns: CodePattern[] = []; if (/private\s+static\s+instance|getInstance\s*\(/.test(content)) { patterns.push({ type: 'singleton', filePath, name: 'Singleton', description: 'Singleton pattern detected' }); } if (/factory|create\w+\s*\(.*\)\s*:\s*\w+/.test(content)) { patterns.push({ type: 'factory', filePath, name: 'Factory', description: 'Factory pattern detected' }); } if (/\.on\s*\(|\.emit\s*\(|EventEmitter|addEventListener/.test(content)) { patterns.push({ type: 'observer', filePath, name: 'Observer', description: 'Observer/Event pattern detected' }); } if (/\.use\s*\(|middleware/i.test(content)) { patterns.push({ type: 'middleware', filePath, name: 'Middleware', description: 'Middleware pattern detected' }); } if (/@\w+\s*\(/.test(content)) { patterns.push({ type: 'decorator', filePath, name: 'Decorator', description: 'Decorator pattern detected' }); } if (/Repository|\.find|\.save|\.delete/i.test(content) && /class\s+\w+Repository/.test(content)) { patterns.push({ type: 'repository', filePath, name: 'Repository', description: 'Repository pattern detected' }); } if (/class\s+\w+Service/.test(content)) { patterns.push({ type: 'service', filePath, name: 'Service', description: 'Service layer pattern detected' }); } if (/class\s+\w+Controller|@Controller/.test(content)) { patterns.push({ type: 'controller', filePath, name: 'Controller', description: 'Controller pattern detected' }); } // Agent pattern: functions/files named *Agent*, LLM-related code, multi-agent orchestration const fileBaseName = filePath.split('/').pop() || ''; if (/[Aa]gent/.test(fileBaseName) || /(?:export\s+)?(?:async\s+)?function\s+\w*[Aa]gent\w*\s*\(/.test(content) || /(?:const|let|var)\s+\w*[Aa]gent\w*\s*=/.test(content)) { // Verify it's an actual agent (has LLM/AI patterns or state/orchestration patterns) const hasLlmPattern = /llm|LLM|ChatModel|BaseChatModel|langchain|openai|anthropic|invoke|generateText|streamText/i.test(content); const hasStatePattern = /state\.|State|graph|Graph|orchestrat|pipeline|workflow/i.test(content); const hasAgentFunc = /(?:export\s+)?(?:async\s+)?function\s+\w*[Aa]gent\w*\s*\(/.test(content); if (hasLlmPattern || hasStatePattern || hasAgentFunc) { patterns.push({ type: 'other', filePath, name: 'Agent', description: 'AI Agent pattern detected - function processes state/LLM interactions' }); } } return patterns; }