-
-
Save srd7/ab6f313ad5952d83ccc3a68d2c4904d3 to your computer and use it in GitHub Desktop.
TypeScript decorator for refactoring
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { refactoring } from './refactoring' | |
| // Mock console.log and console.warn to capture output | |
| const originalConsoleLog = console.log | |
| const originalConsoleWarn = console.warn | |
| let consoleLogCalls: string[] = [] | |
| let consoleWarnCalls: string[] = [] | |
| beforeEach(() => { | |
| consoleLogCalls = [] | |
| consoleWarnCalls = [] | |
| console.log = jest.fn((message: string) => { | |
| consoleLogCalls.push(message) | |
| }) | |
| console.warn = jest.fn((message: string) => { | |
| consoleWarnCalls.push(message) | |
| }) | |
| }) | |
| afterEach(() => { | |
| console.log = originalConsoleLog | |
| console.warn = originalConsoleWarn | |
| }) | |
| describe('Refactoring Decorators', () => { | |
| describe('Synchronous Methods', () => { | |
| class Calculator { | |
| @refactoring('addNew') | |
| add(a: number, b: number): number { | |
| return a + b | |
| } | |
| addNew(a: number, b: number): number { | |
| return a + b | |
| } | |
| @refactoring('multiplyNew') | |
| multiply(a: number, b: number): number { | |
| return a * b | |
| } | |
| multiplyNew(a: number, b: number): number { | |
| // Intentionally different implementation | |
| return a * b + 1 | |
| } | |
| @refactoring('complexObjectNew') | |
| complexObject(id: number): object { | |
| return { | |
| id, | |
| name: 'Test', | |
| details: { | |
| created: '2023-01-01', | |
| active: true, | |
| }, | |
| } | |
| } | |
| complexObjectNew(id: number): object { | |
| return { | |
| id, | |
| name: 'Test', | |
| details: { | |
| created: '2023-01-01', | |
| active: false, // Different value | |
| }, | |
| } | |
| } | |
| normalMethod(): string { | |
| return 'normal' | |
| } | |
| } | |
| it('should return the original result when results are the same', () => { | |
| const calculator = new Calculator() | |
| const result = calculator.add(2, 3) | |
| expect(result).toBe(5) | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in Calculator.add.']) | |
| }) | |
| it('should return the original result and log differences when results differ', () => { | |
| const calculator = new Calculator() | |
| const result = calculator.multiply(2, 3) | |
| expect(result).toBe(6) // Original result | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in Calculator.multiply. original: 6, refactored: 7', | |
| ]) | |
| }) | |
| it('should detect and log nested differences in objects', () => { | |
| const calculator = new Calculator() | |
| const result = calculator.complexObject(1) | |
| expect(result).toEqual({ | |
| id: 1, | |
| name: 'Test', | |
| details: { | |
| created: '2023-01-01', | |
| active: true, | |
| }, | |
| }) | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in Calculator.complexObject. original: {"id":1,"name":"Test","details":{"created":"2023-01-01","active":true}}, refactored: {"id":1,"name":"Test","details":{"created":"2023-01-01","active":false}}', | |
| ]) | |
| }) | |
| }) | |
| describe('Asynchronous Methods', () => { | |
| class AsyncService { | |
| @refactoring('fetchDataNew') | |
| async fetchData(id: string): Promise<object> { | |
| return { id, status: 'success', count: 42 } | |
| } | |
| async fetchDataNew(id: string): Promise<object> { | |
| return { id, status: 'success', count: 42 } | |
| } | |
| @refactoring('processItemNew') | |
| async processItem(item: string): Promise<object> { | |
| return { item, processed: true, timestamp: '2023-01-01' } | |
| } | |
| async processItemNew(item: string): Promise<object> { | |
| return { item, processed: true, timestamp: '2023-01-02' } // Different timestamp | |
| } | |
| } | |
| it('should handle async methods with same results', async () => { | |
| const service = new AsyncService() | |
| const result = await service.fetchData('123') | |
| expect(result).toEqual({ id: '123', status: 'success', count: 42 }) | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in AsyncService.fetchData.']) | |
| }) | |
| it('should handle async methods with different results', async () => { | |
| const service = new AsyncService() | |
| const result = await service.processItem('test') | |
| expect(result).toEqual({ item: 'test', processed: true, timestamp: '2023-01-01' }) | |
| expect(consoleLogCalls[0]).toContain('timestamp') | |
| }) | |
| it('should handle Promise.reject in refactored methods', async () => { | |
| class AsyncRejectService { | |
| @refactoring('fetchWithRejectNew') | |
| async fetchWithReject(id: string): Promise<object> { | |
| return { id, status: 'success' } | |
| } | |
| async fetchWithRejectNew(_id: string): Promise<object> { | |
| return Promise.reject(new Error('Rejected promise in refactored method')) | |
| } | |
| } | |
| const service = new AsyncRejectService() | |
| const result = await service.fetchWithReject('test-id') | |
| // The original method should still work | |
| expect(result).toEqual({ id: 'test-id', status: 'success' }) | |
| // The logger should have been called with the error | |
| expect(consoleWarnCalls).toEqual(['[Refactoring] Error in refactored method fetchWithRejectNew']) | |
| }) | |
| }) | |
| describe('Edge Cases', () => { | |
| it('should log a warning when refactored method does not exist', () => { | |
| class MissingMethodTest { | |
| @refactoring('nonExistentMethod') | |
| testMethod(): string { | |
| return 'original' | |
| } | |
| } | |
| const test = new MissingMethodTest() | |
| const result = test.testMethod() | |
| expect(result).toBe('original') | |
| expect(consoleWarnCalls).toEqual(['[Refactoring] Refactored method nonExistentMethod not found']) | |
| }) | |
| it('should handle exceptions in refactored methods', () => { | |
| class ExceptionTest { | |
| @refactoring('throwingMethod') | |
| safeMethod(): string { | |
| return 'safe result' | |
| } | |
| throwingMethod(): string { | |
| throw new Error('This method throws an error') | |
| } | |
| } | |
| const test = new ExceptionTest() | |
| const result = test.safeMethod() | |
| // The original method should still work | |
| expect(result).toBe('safe result') | |
| expect(consoleWarnCalls).toEqual(['[Refactoring] Error in refactored method throwingMethod']) | |
| }) | |
| it('should not handle exceptions in original methods', () => { | |
| class ExceptionTest { | |
| @refactoring('safeMethod') | |
| throwingMethod(): string { | |
| throw new Error('This method throws an error') | |
| } | |
| safeMethod(): string { | |
| return 'safe result' | |
| } | |
| } | |
| const test = new ExceptionTest() | |
| try { | |
| test.throwingMethod() | |
| // If no error is thrown, the test should fail | |
| fail('Expected an error to be thrown') | |
| } catch (error) { | |
| expect(error).toEqual(new Error('This method throws an error')) | |
| } | |
| }) | |
| it('should handle exceptions in async refactored methods', async () => { | |
| class AsyncExceptionTest { | |
| @refactoring('throwingAsyncMethod') | |
| async safeAsyncMethod(): Promise<string> { | |
| return 'safe async result' | |
| } | |
| async throwingAsyncMethod(): Promise<string> { | |
| throw new Error('This async method throws an error') | |
| } | |
| } | |
| const test = new AsyncExceptionTest() | |
| const result = await test.safeAsyncMethod() | |
| // The original method should still work | |
| expect(result).toBe('safe async result') | |
| expect(consoleWarnCalls).toEqual(['[Refactoring] Error in refactored method throwingAsyncMethod']) | |
| }) | |
| it('should handle primitive values', () => { | |
| class PrimitiveTest { | |
| @refactoring('getStringNew') | |
| getString(): string { | |
| return 'hello' | |
| } | |
| getStringNew(): string { | |
| return 'world' | |
| } | |
| @refactoring('getNumberNew') | |
| getNumber(): number { | |
| return 42 | |
| } | |
| getNumberNew(): number { | |
| return 42 | |
| } | |
| @refactoring('getBooleanNew') | |
| getBoolean(): boolean { | |
| return true | |
| } | |
| getBooleanNew(): boolean { | |
| return false | |
| } | |
| } | |
| const test = new PrimitiveTest() | |
| expect(test.getString()).toBe('hello') | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in PrimitiveTest.getString. original: "hello", refactored: "world"', | |
| ]) | |
| consoleLogCalls = [] | |
| expect(test.getNumber()).toBe(42) | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in PrimitiveTest.getNumber.']) | |
| consoleLogCalls = [] | |
| expect(test.getBoolean()).toBe(true) | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in PrimitiveTest.getBoolean. original: true, refactored: false', | |
| ]) | |
| }) | |
| it('should handle null and undefined', () => { | |
| class NullTest { | |
| @refactoring('getNullNew') | |
| getNull(): null { | |
| return null | |
| } | |
| getNullNew(): null { | |
| return null | |
| } | |
| @refactoring('getUndefinedNew') | |
| getUndefined(): undefined { | |
| return undefined | |
| } | |
| getUndefinedNew(): undefined { | |
| return undefined | |
| } | |
| @refactoring('getNullVsUndefinedNew') | |
| getNullVsUndefined(): null { | |
| return null | |
| } | |
| getNullVsUndefinedNew(): undefined { | |
| return undefined | |
| } | |
| } | |
| const test = new NullTest() | |
| expect(test.getNull()).toBeNull() | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in NullTest.getNull.']) | |
| consoleLogCalls = [] | |
| expect(test.getUndefined()).toBeUndefined() | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in NullTest.getUndefined.']) | |
| consoleLogCalls = [] | |
| expect(test.getNullVsUndefined()).toBeNull() | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in NullTest.getNullVsUndefined. original: null, refactored: undefined', | |
| ]) | |
| }) | |
| }) | |
| describe('Static Methods', () => { | |
| class UnifiedStaticCalculator { | |
| @refactoring('divideNew') | |
| static divide(a: number, b: number): number { | |
| return a / b | |
| } | |
| static divideNew(a: number, b: number): number { | |
| return a / b | |
| } | |
| @refactoring('powerNew') | |
| static power(a: number, b: number): number { | |
| return a ** b | |
| } | |
| static powerNew(a: number, b: number): number { | |
| // Intentionally different implementation | |
| return a ** b + 0.1 | |
| } | |
| @refactoring('getDataNew') | |
| static async getData(id: string): Promise<object> { | |
| return { id, type: 'unified', count: 100 } | |
| } | |
| static async getDataNew(id: string): Promise<object> { | |
| return { id, type: 'unified', count: 101 } | |
| } | |
| } | |
| it('should work with static methods using unified decorators', () => { | |
| const result = UnifiedStaticCalculator.divide(10, 2) | |
| expect(result).toBe(5) | |
| expect(consoleLogCalls).toEqual(['[Refactoring] Implementations match in UnifiedStaticCalculator.divide.']) | |
| }) | |
| it('should detect differences in static methods using unified decorators', () => { | |
| const result = UnifiedStaticCalculator.power(2, 3) | |
| expect(result).toBe(8) // Original result | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in UnifiedStaticCalculator.power. original: 8, refactored: 8.1', | |
| ]) | |
| }) | |
| it('should work with async static methods using unified decorators', async () => { | |
| const result = await UnifiedStaticCalculator.getData('test') | |
| expect(result).toEqual({ id: 'test', type: 'unified', count: 100 }) | |
| expect(consoleLogCalls).toEqual([ | |
| '[Refactoring] Differences detected in UnifiedStaticCalculator.getData. original: {"id":"test","type":"unified","count":100}, refactored: {"id":"test","type":"unified","count":101}', | |
| ]) | |
| }) | |
| }) | |
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function deepEqual(a: any, b: any): boolean { | |
| if (a === b) return true | |
| if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') { | |
| return a === b | |
| } | |
| const keysA = Object.keys(a) | |
| const keysB = Object.keys(b) | |
| if (keysA.length !== keysB.length) return false | |
| return keysA.every(key => keysB.includes(key) && deepEqual(a[key], b[key])) | |
| } | |
| function findDifferences(original: any, refactored: any, path = ''): string[] { | |
| // If values are equal, no differences | |
| if (deepEqual(original, refactored)) return [] | |
| // If not objects or null, they differ at this path | |
| if (original === null || refactored === null || typeof original !== 'object' || typeof refactored !== 'object') { | |
| return [path.length ? path : '(root value)'] | |
| } | |
| const differences: string[] = [] | |
| // Check keys in original | |
| for (const key of Object.keys(original)) { | |
| const newPath = path ? `${path}.${key}` : key | |
| // Key missing in refactored | |
| if (!(key in refactored)) { | |
| differences.push(`${newPath} (missing in refactored)`) | |
| continue | |
| } | |
| // Recursively find differences | |
| differences.push(...findDifferences(original[key], refactored[key], newPath)) | |
| } | |
| // Check for keys in refactored that aren't in original | |
| for (const key of Object.keys(refactored)) { | |
| if (!(key in original)) { | |
| const newPath = path ? `${path}.${key}` : key | |
| differences.push(`${newPath} (missing in original)`) | |
| } | |
| } | |
| return differences | |
| } | |
| function isStaticMethod(target: any): boolean { | |
| return typeof target === 'function' || target.constructor === Function | |
| } | |
| function getMethodName(target: any, propertyKey: string): string { | |
| if (isStaticMethod(target)) { | |
| // For static methods, target is the constructor function | |
| return `${target.name || 'Anonymous'}.${propertyKey}` | |
| } | |
| // For instance methods, target is the prototype, so we need to get the constructor | |
| const className = target.constructor?.name || 'Anonymous' | |
| return `${className}.${propertyKey}` | |
| } | |
| function getRefactoredMethod(target: any, refactoredMethodName: string, thisArg: any) { | |
| try { | |
| if (isStaticMethod(target)) { | |
| return (target as any)[refactoredMethodName] | |
| } | |
| return (thisArg as any)[refactoredMethodName] | |
| } catch (error) { | |
| return undefined | |
| } | |
| } | |
| function logResults(methodName: string, originalResult: any, refactoredResult: any): void { | |
| const differences = findDifferences(originalResult, refactoredResult) | |
| if (differences.length > 0) { | |
| console.log( | |
| `[Refactoring] Differences detected in ${methodName}. original: ${JSON.stringify(originalResult)}, refactored: ${JSON.stringify(refactoredResult)}` | |
| ) | |
| } else { | |
| console.log(`[Refactoring] Implementations match in ${methodName}.`) | |
| } | |
| } | |
| export function refactoring(refactoredMethodName: string) { | |
| return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> => { | |
| const originalMethod = descriptor.value | |
| const methodName = getMethodName(target, propertyKey) | |
| // Create a new method that will call both implementations | |
| descriptor.value = function (this: any, ...args: any[]) { | |
| // Get the refactored method from the target | |
| const refactoredMethod = getRefactoredMethod(target, refactoredMethodName, this) | |
| // Check if the refactored method exists | |
| if (!refactoredMethod) { | |
| console.warn(`[Refactoring] Refactored method ${refactoredMethodName} not found`) | |
| return originalMethod.apply(this, args) | |
| } | |
| const originalResult = originalMethod.apply(this, args) | |
| // Handle async methods | |
| if (originalResult instanceof Promise) { | |
| return originalResult.then(async (resolvedOriginalResult: any) => { | |
| // Call the refactored method after the original completes | |
| try { | |
| const refactoredResult = await Promise.resolve(refactoredMethod.apply(this, args)) | |
| // Compare results | |
| logResults(methodName, resolvedOriginalResult, refactoredResult) | |
| return resolvedOriginalResult | |
| } catch (error) { | |
| // Log the error but don't let it affect the original method | |
| console.warn(`[Refactoring] Error in refactored method ${refactoredMethodName}`) | |
| return resolvedOriginalResult | |
| } | |
| }) | |
| } | |
| // For synchronous methods | |
| try { | |
| const refactoredResult = refactoredMethod.apply(this, args) | |
| // Compare results | |
| logResults(methodName, originalResult, refactoredResult) | |
| } catch (error: any) { | |
| // Log the error but don't let it affect the original method | |
| console.warn(`[Refactoring] Error in refactored method ${refactoredMethodName}`) | |
| } | |
| // Return the original result | |
| return originalResult | |
| } as any | |
| return descriptor | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment