import { type ClassDeclaration, type MethodDeclaration, Scope } from 'ts-morph'; /** * Metadata about a property used by methods. */ export interface PropertyUsage { name: string; // Property name (e.g., '_configParams') type: string; // Property type (e.g., 'IConfigParam[]') scope: Scope; // Private, Public, Protected isReadonly: boolean; // Readonly modifier accessType: 'read' | 'write' | 'both'; // How property is used initializer?: string; // Property initializer (e.g., '[]', '{}') } /** * Analyzes which properties are used by which methods. * * This helper scans method bodies for property accesses and extracts * full property metadata from the class declaration. * * @example * ```typescript * const analyzer = new PropertyDependencyAnalyzer(); * const properties = analyzer.analyzePropertyUsage(classDecl, methods); * // Returns: [{ name: '_configParams', type: 'IConfigParam[]', scope: Private, ... }] * ``` */ export class PropertyDependencyAnalyzer { /** * Analyzes which properties are used by the provided methods. * * Detection Strategy: * - Scans method bodies for `this.propertyName` patterns * - Determines read vs write access (assignment vs usage) * - Extracts property declarations from original class * - Returns full property metadata for code generation * * @param classDecl - The class declaration containing the properties * @param methods - The methods to analyze for property usage * @returns Array of property usage metadata */ analyzePropertyUsage( classDecl: ClassDeclaration, methods: MethodDeclaration[], ): PropertyUsage[] { const propertyUsageMap = new Map(); // Scan all method bodies for property accesses for (const method of methods) { const bodyText = method.getBodyText() ?? ''; // Find all property accesses: this.propertyName const propertyPattern = /this\.(\w+)/g; let match: RegExpExecArray | null; while ((match = propertyPattern.exec(bodyText)) !== null) { const propertyName = match[1]; if (!propertyName) { continue; } // Skip if we've already analyzed this property if (propertyUsageMap.has(propertyName)) { continue; } // Extract property metadata from class const propertyMetadata = this.extractPropertyMetadata( classDecl, propertyName, ); if (!propertyMetadata) { continue; } // Determine access type (read/write/both) propertyMetadata.accessType = this.determineAccessType( bodyText, propertyName, ); propertyUsageMap.set(propertyName, propertyMetadata); } } return Array.from(propertyUsageMap.values()); } /** * Extracts property declaration metadata from class. * Includes type, scope, readonly, initializer. * * @param classDecl - The class declaration * @param propertyName - The property name to extract * @returns Property metadata or undefined if not found */ private extractPropertyMetadata( classDecl: ClassDeclaration, propertyName: string, ): PropertyUsage | undefined { const property = classDecl.getProperty(propertyName); if (!property) { return undefined; } const type = property.getType().getText(property); const scope = property.getScope() ?? Scope.Public; const isReadonly = property.isReadonly(); const initializer = property.getInitializer()?.getText(); return { name: propertyName, type: type ?? 'any', scope, isReadonly, accessType: 'read', // Will be updated by determineAccessType initializer, }; } /** * Determines if property is read, written, or both in method body. * * @param methodBody - The method body text * @param propertyName - The property name to check * @returns Access type: 'read', 'write', or 'both' */ private determineAccessType( methodBody: string, propertyName: string, ): 'read' | 'write' | 'both' { // Check for write access: this.propertyName = const writePattern = new RegExp(String.raw`this\.${propertyName}\s*=`, 'g'); const hasWrite = writePattern.test(methodBody); // Check for read access: this.propertyName (not followed by =) const readPattern = new RegExp( String.raw`this\.${propertyName}(?!\s*=)`, 'g', ); const hasRead = readPattern.test(methodBody); if (hasWrite && hasRead) { return 'both'; } else if (hasWrite) { return 'write'; } else { return 'read'; } } }