import { type ClassDeclaration, type MethodDeclaration, Scope } from 'ts-morph'; import { type PropertyDependencyAnalyzer } from './property-dependency-analyzer.js'; /** * Metadata about a property shared across multiple responsibilities. */ export interface SharedProperty { name: string; // Property name type: string; // Property type scope: Scope; // Property scope (Private, Public, Protected) usedByResponsibilities: string[]; // Which responsibilities use it initializer?: string; // Property initializer } /** * Specification for generating a state service. */ export interface StateServiceSpec { serviceName: string; // e.g., 'ConfigStateService' sharedProperties: SharedProperty[]; originalClassName: string; // e.g., 'ConfigService' } /** * Responsibility group with methods. */ export interface ResponsibilityGroup { responsibility: string; methods: MethodDeclaration[]; } /** * Detects properties used by multiple responsibility groups. * * Generates state service specifications for shared properties. * * @example * ```typescript * const detector = new SharedStateDetector(); * const spec = detector.detectSharedState(classDecl, groups, propertyAnalyzer); * if (spec) { * const code = detector.generateStateServiceCode(spec); * } * ``` */ export class SharedStateDetector { /** * Detects properties used by >1 responsibility group. * * Algorithm: * 1. Track property usage per responsibility group * 2. Find properties used by >1 group * 3. Generate state service specification * * @param classDecl - The class declaration * @param responsibilityGroups - Groups of methods by responsibility * @param propertyAnalyzer - Property dependency analyzer * @returns State service specification or null if no shared properties */ detectSharedState( classDecl: ClassDeclaration, responsibilityGroups: ResponsibilityGroup[], propertyAnalyzer: PropertyDependencyAnalyzer, ): StateServiceSpec | null { // Map: propertyName -> Set const propertyUsageMap = new Map>(); // Track which responsibilities use which properties for (const group of responsibilityGroups) { const properties = propertyAnalyzer.analyzePropertyUsage( classDecl, group.methods, ); for (const prop of properties) { if (!propertyUsageMap.has(prop.name)) { propertyUsageMap.set(prop.name, new Set()); } propertyUsageMap.get(prop.name)!.add(group.responsibility); } } // Find properties used by more than one responsibility const sharedProperties: SharedProperty[] = []; for (const [propertyName, responsibilities] of propertyUsageMap.entries()) { if (responsibilities.size > 1) { // Get full property metadata const property = classDecl.getProperty(propertyName); if (property) { sharedProperties.push({ name: propertyName, type: property.getType().getText(property), scope: property.getScope() ?? Scope.Public, usedByResponsibilities: Array.from(responsibilities), initializer: property.getInitializer()?.getText(), }); } } } // Return null if no shared properties if (sharedProperties.length === 0) { return null; } // Generate state service specification const originalClassName = classDecl.getName() ?? 'Unknown'; // Strip trailing "Service" from originalClassName, then append "StateService" // e.g., "ConfigService" -> "ConfigStateService" (not "ConfigServiceState") const baseClassName = originalClassName.replace(/Service$/, ''); const serviceName = `${baseClassName}StateService`; return { serviceName, sharedProperties, originalClassName, }; } /** * Generates state service source code. * Creates getter/setter methods for shared properties. * * @param spec - State service specification * @returns Generated source code */ generateStateServiceCode(spec: StateServiceSpec): string { const lines: string[] = []; // Imports lines.push(`import { Injectable } from '@angular/core';`); lines.push(''); // Class declaration lines.push(`@Injectable({ providedIn: 'root' })`); lines.push(`export class ${spec.serviceName} {`); // Properties and getters/setters for (const prop of spec.sharedProperties) { const getterName = prop.name.replace(/^_/, ''); const initializer = prop.initializer ? ` = ${prop.initializer}` : ''; // Private property lines.push(` private ${prop.name}: ${prop.type}${initializer};`); lines.push(''); // Getter lines.push(` get ${getterName}(): ${prop.type} {`); lines.push(` return this.${prop.name};`); lines.push(` }`); lines.push(''); // Setter lines.push(` set ${getterName}(value: ${prop.type}) {`); lines.push(` this.${prop.name} = value;`); lines.push(` }`); lines.push(''); } lines.push('}'); return lines.join('\n'); } }