main
ts 153 lines 4.29 KB
Raw
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;