Skip to content

Instantly share code, notes, and snippets.

@srd7
Created April 17, 2025 01:14
Show Gist options
  • Select an option

  • Save srd7/ab6f313ad5952d83ccc3a68d2c4904d3 to your computer and use it in GitHub Desktop.

Select an option

Save srd7/ab6f313ad5952d83ccc3a68d2c4904d3 to your computer and use it in GitHub Desktop.
TypeScript decorator for refactoring
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}',
])
})
})
})
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