import { type ClassDeclaration, type ConstructorDeclaration } from 'ts-morph'; /** * Metadata about property initialization in constructor. */ export interface PropertyInitialization { propertyName: string; // Property being initialized initializationCode: string; // Code to initialize (e.g., 'new BehaviorSubject(null)') } /** * Extracts constructor initialization logic for properties. * * Supports the following initialization patterns: * * 1. **RxJS Subjects:** * ```typescript * this.dataStream = new BehaviorSubject(initialData); * this.events = new Subject(); * ``` * * 2. **Dependency-based initialization:** * ```typescript * this.apiUrl = this.config.get('apiUrl'); * this.timeout = this.config.timeout || DEFAULT_TIMEOUT; * ``` * * 3. **Method calls:** * ```typescript * this.transformedData = this.processData(initialData); * ``` * * 4. **Complex expressions:** * ```typescript * this.isEnabled = environment.production && this.config.enabled; * ``` * * 5. **Constructor parameters:** * ```typescript * this.initialConfig = { ...config }; * ``` * * @example * ```typescript * const analyzer = new ConstructorInitializationAnalyzer(); * const inits = analyzer.extractPropertyInitializations(classDecl, ['dataStream', 'apiUrl']); * // Returns: * // [ * // { propertyName: 'dataStream', initializationCode: 'new BehaviorSubject(initialData)' }, * // { propertyName: 'apiUrl', initializationCode: "this.config.get('apiUrl')" } * // ] * ``` */ export class ConstructorInitializationAnalyzer { /** * Extracts constructor initialization statements for specific properties. * * Focuses on: * - RxJS Subject/BehaviorSubject instantiation * - Property assignments in constructor body * - Initialization with dependencies * * @param classDecl - The class declaration * @param propertyNames - Names of properties to extract initializations for * @returns Array of property initialization metadata */ extractPropertyInitializations( classDecl: ClassDeclaration, propertyNames: string[], ): PropertyInitialization[] { const constructors = classDecl.getConstructors(); if (constructors.length === 0) { return []; } const constructor = constructors[0]; if (!constructor) { return []; } return this.parseConstructorBody(constructor, propertyNames); } /** * Parses constructor body to find initialization statements. * * Handles various patterns: * - Simple assignments: `this.propertyName = value;` * - RxJS instantiation: `this.propertyName = new Subject()` * - Dependency calls: `this.propertyName = this.service.method()` * - Complex expressions: `this.propertyName = expr1 && expr2` * - Multi-line objects: `this.propertyName = { ... }` * * The regex pattern captures everything between `=` and the next semicolon, * including multi-line statements and complex object literals. * * @param constructor - The constructor declaration * @param propertyNames - Names of properties to extract initializations for * @returns Array of property initialization metadata */ private parseConstructorBody( constructor: ConstructorDeclaration, propertyNames: string[], ): PropertyInitialization[] { const bodyText = constructor.getBodyText() ?? ''; const initializations: PropertyInitialization[] = []; for (const propertyName of propertyNames) { // Enhanced pattern: Handle multi-line and complex expressions // Match: this.propertyName = // The pattern captures: // 1. Everything until a semicolon (standard case) // 2. Multi-line statements with nested braces // 3. Stops at the next `this.` statement or end of constructor const pattern = new RegExp( String.raw`this\.${propertyName}\s*=\s*([^;]+(?:;|(?=\s*this\.)|$))`, 'gms', // g=global, m=multiline, s=dotAll (. matches newlines) ); const match = pattern.exec(bodyText); if (match?.[1]) { let initializationCode = match[1].trim(); // Remove trailing semicolon if present if (initializationCode.endsWith(';')) { initializationCode = initializationCode.slice(0, -1).trim(); } initializations.push({ propertyName, initializationCode, }); } } return initializations; } }