|
| 1 | +import { descriptorForProperty, isComputed } from '@ember/-internals/metal'; |
| 2 | +import type { ComputedMetadata } from '../types'; |
| 3 | + |
| 4 | +export const computed = { |
| 5 | + /** |
| 6 | + * Check if a property is a computed property. |
| 7 | + */ |
| 8 | + isComputed(obj: object, key: string): boolean { |
| 9 | + try { |
| 10 | + return isComputed(obj, key); |
| 11 | + } catch { |
| 12 | + return false; |
| 13 | + } |
| 14 | + }, |
| 15 | + |
| 16 | + /** |
| 17 | + * Get the computed property descriptor for a property. |
| 18 | + * Returns null if the property is not computed. |
| 19 | + */ |
| 20 | + getComputedPropertyDescriptor(obj: object, key: string): unknown | null { |
| 21 | + try { |
| 22 | + return descriptorForProperty(obj, key) ?? null; |
| 23 | + } catch { |
| 24 | + return null; |
| 25 | + } |
| 26 | + }, |
| 27 | + |
| 28 | + /** |
| 29 | + * Get the dependent keys for a computed property. |
| 30 | + * Returns an empty array if the property is not computed or has no dependent keys. |
| 31 | + */ |
| 32 | + getDependentKeys(obj: object, key: string): string[] { |
| 33 | + try { |
| 34 | + const desc = descriptorForProperty(obj, key) as any; |
| 35 | + return desc?._dependentKeys ?? []; |
| 36 | + } catch { |
| 37 | + return []; |
| 38 | + } |
| 39 | + }, |
| 40 | + |
| 41 | + /** |
| 42 | + * Get computed property metadata without accessing private properties directly. |
| 43 | + * Replaces direct access to desc._getter, desc._readOnly, desc._auto, etc. |
| 44 | + * |
| 45 | + * @param descriptor - The computed property descriptor |
| 46 | + * @returns Public metadata object, or null if descriptor is invalid |
| 47 | + */ |
| 48 | + getComputedMetadata(descriptor: unknown): ComputedMetadata | null { |
| 49 | + if (!descriptor) { |
| 50 | + return null; |
| 51 | + } |
| 52 | + |
| 53 | + const desc = descriptor as any; |
| 54 | + |
| 55 | + const getter: Function | undefined = desc._getter ?? desc.get; |
| 56 | + const setter: Function | undefined = desc.set; |
| 57 | + |
| 58 | + return { |
| 59 | + getter, |
| 60 | + setter, |
| 61 | + readOnly: desc._readOnly ?? false, |
| 62 | + auto: desc._auto ?? false, |
| 63 | + dependentKeys: desc._dependentKeys ?? [], |
| 64 | + code: getter ? Function.prototype.toString.call(getter) : undefined, |
| 65 | + }; |
| 66 | + }, |
| 67 | + |
| 68 | + /** |
| 69 | + * Check if a descriptor is Ember's mandatory setter. |
| 70 | + * Replaces checking for "You attempted to update" string in setter code. |
| 71 | + * |
| 72 | + * @param descriptor - The property descriptor |
| 73 | + * @returns true if this is a mandatory setter |
| 74 | + */ |
| 75 | + isMandatorySetter(descriptor: unknown): boolean { |
| 76 | + if (!descriptor) return false; |
| 77 | + |
| 78 | + const desc = descriptor as any; |
| 79 | + if (typeof desc.set !== 'function') return false; |
| 80 | + |
| 81 | + try { |
| 82 | + return Function.prototype.toString.call(desc.set).includes('You attempted to update'); |
| 83 | + } catch { |
| 84 | + return false; |
| 85 | + } |
| 86 | + }, |
| 87 | + |
| 88 | + /** |
| 89 | + * Check if a property uses the @cached decorator from @glimmer/tracking. |
| 90 | + * The @cached decorator memoizes getter results and invalidates when dependencies change. |
| 91 | + * |
| 92 | + * @param obj - The object |
| 93 | + * @param key - The property name |
| 94 | + * @returns true if the property uses @cached decorator |
| 95 | + */ |
| 96 | + isCached(obj: object, key: string): boolean { |
| 97 | + try { |
| 98 | + // @cached replaces the getter with one that uses createCache/getValue from @glimmer/validator |
| 99 | + // It is NOT a ComputedProperty descriptor - it's a native getter |
| 100 | + if (this.isComputed(obj, key)) { |
| 101 | + return false; |
| 102 | + } |
| 103 | + |
| 104 | + // Check the prototype chain for a native getter |
| 105 | + let proto: object | null = Object.getPrototypeOf(obj); |
| 106 | + while (proto !== null && proto !== Object.prototype) { |
| 107 | + const nativeDesc = Object.getOwnPropertyDescriptor(proto, key); |
| 108 | + if (nativeDesc?.get) { |
| 109 | + // @cached wraps the getter with a WeakMap-based cache using @glimmer/validator's createCache |
| 110 | + // We detect it by checking if the getter's source references the cache WeakMap pattern |
| 111 | + // This is a best-effort heuristic |
| 112 | + const src = Function.prototype.toString.call(nativeDesc.get); |
| 113 | + if (src.includes('caches') && src.includes('getValue')) { |
| 114 | + return true; |
| 115 | + } |
| 116 | + break; |
| 117 | + } |
| 118 | + proto = Object.getPrototypeOf(proto); |
| 119 | + } |
| 120 | + |
| 121 | + return false; |
| 122 | + } catch { |
| 123 | + return false; |
| 124 | + } |
| 125 | + }, |
| 126 | +}; |
0 commit comments