| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | 'use strict'; |
| 9 | |
| 10 | type ROViolationType = |
| 11 | | 'FORGET_MUTATE_IMMUT' |
| 12 | | 'FORGET_DELETE_PROP_IMMUT' |
| 13 | | 'FORGET_CHANGE_PROP_IMMUT' |
| 14 | | 'FORGET_ADD_PROP_IMMUT'; |
| 15 | type ROViolationLogger = ( |
| 16 | violation: ROViolationType, |
| 17 | source: string, |
| 18 | key: string, |
| 19 | value?: any, |
| 20 | ) => void; |
| 21 | |
| 22 | /** |
| 23 | * Represents a "proxy" of a read-only object property |
| 24 | * savedVal: underlying "source of truth" for a property value |
| 25 | * getter: hack, this lets us check whether we have already saved this property |
| 26 | * */ |
| 27 | type SavedEntry = { |
| 28 | savedVal: unknown; |
| 29 | getter: () => unknown; |
| 30 | }; |
| 31 | type SavedROObject = Map<string, SavedEntry>; |
| 32 | type SavedROObjects = WeakMap<Object, SavedROObject>; |
| 33 | |
| 34 | // Utility functions |
| 35 | function isWriteable(desc: PropertyDescriptor) { |
| 36 | return (desc.writable || desc.set) && desc.configurable; |
| 37 | } |
| 38 | |
| 39 | function getOrInsertDefault( |
| 40 | m: SavedROObjects, |
| 41 | k: object, |
| 42 | ): {existed: boolean; entry: SavedROObject} { |
| 43 | const entry = m.get(k); |
| 44 | if (entry) { |
| 45 | return {existed: true, entry}; |
| 46 | } else { |
| 47 | const newEntry: SavedROObject = new Map(); |
| 48 | m.set(k, newEntry); |
| 49 | return {existed: false, entry: newEntry}; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | function buildMakeReadOnly( |
| 54 | logger: ROViolationLogger, |
| 55 | skippedClasses: string[], |
| 56 | ): <T>(val: T, source: string) => T { |
| 57 | // All saved proxys |
| 58 | const savedROObjects: SavedROObjects = new WeakMap(); |
| 59 | |
| 60 | // Overwrites an object property with its proxy and saves its original value |
| 61 | function addProperty( |
| 62 | obj: Object, |
| 63 | source: string, |
| 64 | key: string, |
| 65 | prop: PropertyDescriptor, |
| 66 | savedEntries: Map<string, SavedEntry>, |
| 67 | ) { |
| 68 | const proxy: PropertyDescriptor & {get(): unknown} = { |
| 69 | get() { |
| 70 | // read from backing cache entry |
| 71 | return makeReadOnly(savedEntries.get(key)!.savedVal, source); |
| 72 | }, |
| 73 | set(newVal: unknown) { |
| 74 | logger('FORGET_MUTATE_IMMUT', source, key, newVal); |
| 75 | // update backing cache entry |
| 76 | savedEntries.get(key)!.savedVal = newVal; |
| 77 | }, |
| 78 | }; |
| 79 | if (prop.configurable != null) { |
| 80 | proxy.configurable = prop.configurable; |
| 81 | } |
| 82 | if (prop.enumerable != null) { |
| 83 | proxy.enumerable = prop.enumerable; |
| 84 | } |
| 85 | |
| 86 | savedEntries.set(key, {savedVal: (obj as any)[key], getter: proxy.get}); |
| 87 | Object.defineProperty(obj, key, proxy); |
| 88 | } |
| 89 | |
| 90 | // Changes an object to be read-only, returns its input |
| 91 | function makeReadOnly<T>(o: T, source: string): T { |
| 92 | if (typeof o !== 'object' || o == null) { |
| 93 | return o; |
| 94 | } else if ( |
| 95 | o.constructor?.name != null && |
| 96 | skippedClasses.includes(o.constructor.name) |
| 97 | ) { |
| 98 | return o; |
| 99 | } |
| 100 | |
| 101 | const {existed, entry: cache} = getOrInsertDefault(savedROObjects, o); |
| 102 | |
| 103 | for (const [k, entry] of cache.entries()) { |
| 104 | const currentProp = Object.getOwnPropertyDescriptor(o, k); |
| 105 | if (currentProp && !isWriteable(currentProp)) { |
| 106 | continue; |
| 107 | } |
| 108 | const currentPropGetter = currentProp?.get; |
| 109 | const cachedGetter = entry.getter; |
| 110 | |
| 111 | if (currentPropGetter !== cachedGetter) { |
| 112 | // cache is currently holding an old property |
| 113 | // - it may have been deleted |
| 114 | // - it may have been deleted + re-set |
| 115 | // (meaning that new value is not proxied, |
| 116 | // and the current proxied value is stale) |
| 117 | cache.delete(k); |
| 118 | if (!currentProp) { |
| 119 | logger('FORGET_DELETE_PROP_IMMUT', source, k); |
| 120 | } else if (currentProp) { |
| 121 | logger('FORGET_CHANGE_PROP_IMMUT', source, k); |
| 122 | addProperty(o, source, k, currentProp, cache); |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | for (const [k, prop] of Object.entries( |
| 127 | Object.getOwnPropertyDescriptors(o), |
| 128 | )) { |
| 129 | if (!cache.has(k) && isWriteable(prop)) { |
| 130 | if ( |
| 131 | prop.hasOwnProperty('set') || |
| 132 | prop.hasOwnProperty('get') || |
| 133 | k === 'current' |
| 134 | ) { |
| 135 | // - we currently don't handle accessor properties |
| 136 | // - we currently have no other way of checking whether an object |
| 137 | // is a `ref` (i.e. returned by useRef). |
| 138 | continue; |
| 139 | } |
| 140 | |
| 141 | if (existed) { |
| 142 | logger('FORGET_ADD_PROP_IMMUT', source, k); |
| 143 | } |
| 144 | addProperty(o, source, k, prop, cache); |
| 145 | } |
| 146 | } |
| 147 | return o; |
| 148 | } |
| 149 | |
| 150 | return makeReadOnly; |
| 151 | } |
| 152 | |
| 153 | export default buildMakeReadOnly; |