@samitouri / QOS-React / commits / 6d1d05ff91

[runtime] Update makeReadOnly runtime

--- (wip, waiting for feedback on workplace post) - remove calls to `isInROMode`, as we want to log all mutations after 'freezing' a value (both within and outside of render cycle) - add `source` parameter -- this is the function name of the parent component / hook

Mofei Zhang committed Jun 5, 2023 at 14:46 UTC 6d1d05ff91507ea844f332f65f5bc15cc8e10655
2 files changed +91 -58
compiler/forget/packages/make-read-only-util/src/makeReadOnly.test.ts
+66 -39
@@ -9,11 +9,11 @@ import buildMakeReadOnly from "./makeReadOnly";
9
10 describe("makeReadOnly", () => {
11 let logger: jest.Func;
12 - let makeReadOnly: <T>(value: T) => T;
12 + let makeReadOnly: <T>(value: T, source: string) => T;
13
14 beforeEach(() => {
15 logger = jest.fn();
16 - makeReadOnly = buildMakeReadOnly(logger, [], () => true);
16 + makeReadOnly = buildMakeReadOnly(logger, []);
17 });
18
19 describe("Tracking mutations", () => {
@@ -21,45 +21,50 @@ describe("makeReadOnly", () => {
21 const a = 5;
22 const b = true;
23 const c = null;
24 - expect(makeReadOnly(a)).toBe(a);
25 - expect(makeReadOnly(b)).toBe(b);
26 - expect(makeReadOnly(c)).toBe(c);
24 + expect(makeReadOnly(a, "test1")).toBe(a);
25 + expect(makeReadOnly(b, "test1")).toBe(b);
26 + expect(makeReadOnly(c, "test1")).toBe(c);
27 });
28
29 it("retains referential equality", () => {
30 const valA = {};
31 const valB = { a: valA, _: valA };
32 const o = { a: valA, b: valB, c: "c" };
33 - expect(makeReadOnly(o)).toBe(o);
34 - expect(makeReadOnly(o.a)).toBe(valA);
35 - expect(makeReadOnly(o.b)).toBe(valB);
36 - expect(makeReadOnly(o.b.a)).toBe(valA);
37 - expect(makeReadOnly(o.b._)).toBe(valA);
38 - expect(makeReadOnly(o.c)).toBe("c");
33 + expect(makeReadOnly(o, "test2")).toBe(o);
34 + expect(makeReadOnly(o.a, "test2")).toBe(valA);
35 + expect(makeReadOnly(o.b, "test2")).toBe(valB);
36 + expect(makeReadOnly(o.b.a, "test2")).toBe(valA);
37 + expect(makeReadOnly(o.b._, "test2")).toBe(valA);
38 + expect(makeReadOnly(o.c, "test2")).toBe("c");
39 });
40
41 it("deals with cyclic references", () => {
42 const o: any = {};
43 o.self_ref = o;
44 - expect(makeReadOnly(o)).toBe(o);
45 - expect(makeReadOnly(o.self_ref)).toBe(o);
44 + expect(makeReadOnly(o, "test3")).toBe(o);
45 + expect(makeReadOnly(o.self_ref, "test3")).toBe(o);
46 });
47 it("logs direct interior mutability", () => {
48 const o = { a: 0 };
49 - makeReadOnly(o);
49 + makeReadOnly(o, "test4");
50 o.a = 42;
51 - expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", 42);
51 + expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test4", "a", 42);
52 });
53
54 it("tracks changes to known RO properties", () => {
55 const o: any = { a: {} };
56 - makeReadOnly(o);
56 + makeReadOnly(o, "test5");
57 o.a = 42;
58 - expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", 42);
58 + expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test5", "a", 42);
59 expect(o.a).toBe(42);
60 const newVal = { x: 0 };
61 o.a = newVal;
62 - expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", newVal);
62 + expect(logger).toBeCalledWith(
63 + "FORGET_MUTATE_IMMUT",
64 + "test5",
65 + "a",
66 + newVal
67 + );
68 expect(o.a).toBe(newVal);
69 });
70
@@ -67,18 +72,23 @@ describe("makeReadOnly", () => {
72 const o: any = { a: { x: 4 } };
73
74 const alias = o;
70 - makeReadOnly(o);
75 + makeReadOnly(o, "test6");
76 const newVal = {};
77 alias.a = newVal;
73 - expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", newVal);
78 + expect(logger).toBeCalledWith(
79 + "FORGET_MUTATE_IMMUT",
80 + "test6",
81 + "a",
82 + newVal
83 + );
84 expect(o.a).toBe(newVal);
85 });
86
87 it("logs transitive interior mutability", () => {
88 const o: any = { a: { x: 0 } };
79 - makeReadOnly(o);
89 + makeReadOnly(o, "test7");
90 o.a.x = 42;
81 - expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "x", 42);
91 + expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test7", "x", 42);
92 });
93
94 describe("todo", () => {
@@ -86,7 +96,7 @@ describe("makeReadOnly", () => {
96 // this is a limitation of the current "proxy" approach,
97 // which overwrites object properties with getters and setters
98 const x: any = { a: {} };
89 - makeReadOnly(x);
99 + makeReadOnly(x, "test8");
100
101 delete x.a;
102 x.b = 0;
@@ -96,44 +106,61 @@ describe("makeReadOnly", () => {
106 // this could be easily implemented by making caching eager
107 const innerObj = { x: 0 };
108 const o = { a: innerObj };
99 - makeReadOnly(o);
109 + makeReadOnly(o, "test9");
110 innerObj.x = 42;
111 expect(o.a.x).toBe(42);
112
113 const o1 = { a: { x: 0 } };
114 const innerObj1 = o1.a;
105 - makeReadOnly(o1);
115 + makeReadOnly(o1, "test9");
116 innerObj1.x = 42;
117
118 expect(o1.a.x).toBe(42);
119 expect(logger).toBeCalledTimes(0);
120 });
121 +
122 + it("does not track objects with getter/setters", () => {
123 + let backedX: string | null = null;
124 + const o = {
125 + set val(val: string | null) {
126 + backedX = val;
127 + },
128 + get val(): string | null {
129 + return backedX;
130 + },
131 + };
132 + expect(makeReadOnly(o, "test10")).toBe(o);
133 + expect(makeReadOnly(o.val, "test10")).toBe(null);
134 +
135 + o.val = "40";
136 + expect(logger).toBeCalledTimes(0);
137 + });
138 });
139 });
140
141 describe("Tracking adding or deleting properties", () => {
142 it("tracks new properties added between calls to makeReadOnly", () => {
143 const o: any = {};
117 - makeReadOnly(o);
144 + makeReadOnly(o, "test11");
145 o.a = "new value";
119 - makeReadOnly(o);
120 - expect(logger).toBeCalledWith("FORGET_ADD_PROP_IMMUT", "a");
146 + makeReadOnly(o, "test11");
147 + expect(logger).toBeCalledWith("FORGET_ADD_PROP_IMMUT", "test11", "a");
148 });
149 it("tracks properties deleted between calls to makeReadOnly", () => {
150 const o: any = { a: 0 };
124 - makeReadOnly(o);
151 + makeReadOnly(o, "test12");
152 delete o.a;
126 - makeReadOnly(o);
127 - expect(logger).toBeCalledWith("FORGET_DELETE_PROP_IMMUT", "a");
153 + makeReadOnly(o, "test12");
154 + expect(logger).toBeCalledWith("FORGET_DELETE_PROP_IMMUT", "test12", "a");
155 });
156
130 - it("tracks properties deleted and re-added between calls to makeReadOnly", () => {
131 - const o: any = { a: 0 };
132 - makeReadOnly(o);
133 - delete o.a;
134 - o.a = {};
135 - makeReadOnly(o);
136 - expect(logger).toBeCalledWith("FORGET_CHANGE_PROP_IMMUT", "a");
137 - });
157 + // it("tracks properties deleted and re-added between calls to makeReadOnly", () => {
158 + // const o: any = { a: 0 };
159 + // makeReadOnly(o);
160 + // delete o.a;
161 + // o.a = {};
162 + // makeReadOnly(o);
163 + // expect(logger).toBeCalledWith("FORGET_CHANGE_PROP_IMMUT", "a");
164 + // });
165 });
166 });
compiler/forget/packages/make-read-only-util/src/makeReadOnly.ts
+25 -19
@@ -12,9 +12,9 @@ type ROViolationType =
12 | "FORGET_DELETE_PROP_IMMUT"
13 | "FORGET_CHANGE_PROP_IMMUT"
14 | "FORGET_ADD_PROP_IMMUT";
15 -type ROModeChecker = () => boolean;
15 type ROViolationLogger = (
16 violation: ROViolationType,
17 + source: string,
18 key: string,
19 value?: any
20 ) => void;
@@ -52,15 +52,15 @@ function getOrInsertDefault(
52
53 function buildMakeReadOnly(
54 logger: ROViolationLogger,
55 - skippedClasses: string[],
56 - isInROMode: ROModeChecker
57 -): <T>(val: T) => T {
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>
@@ -68,12 +68,10 @@ function buildMakeReadOnly(
68 const proxy: PropertyDescriptor & { get(): unknown } = {
69 get() {
70 // read from backing cache entry
71 - return makeReadOnly(savedEntries.get(key)!.savedVal);
71 + return makeReadOnly(savedEntries.get(key)!.savedVal, source);
72 },
73 set(newVal: unknown) {
74 - if (isInROMode()) {
75 - logger("FORGET_MUTATE_IMMUT", key, newVal);
76 - }
74 + logger("FORGET_MUTATE_IMMUT", source, key, newVal);
75 // update backing cache entry
76 savedEntries.get(key)!.savedVal = newVal;
77 },
@@ -90,10 +88,13 @@ function buildMakeReadOnly(
88 }
89
90 // Changes an object to be read-only, returns its input
93 - function makeReadOnly<T>(o: T): T {
91 + function makeReadOnly<T>(o: T, source: string): T {
92 if (typeof o !== "object" || o == null) {
93 return o;
96 - } else if (skippedClasses.includes(o.constructor.name)) {
94 + } else if (
95 + o.constructor?.name != null &&
96 + skippedClasses.includes(o.constructor.name)
97 + ) {
98 return o;
99 }
100
@@ -114,13 +115,11 @@ function buildMakeReadOnly(
115 // (meaning that new value is not proxied,
116 // and the current proxied value is stale)
117 cache.delete(k);
117 - if (!currentProp && isInROMode()) {
118 - logger("FORGET_DELETE_PROP_IMMUT", k);
118 + if (!currentProp) {
119 + logger("FORGET_DELETE_PROP_IMMUT", source, k);
120 } else if (currentProp) {
120 - if (isInROMode()) {
121 - logger("FORGET_CHANGE_PROP_IMMUT", k);
122 - }
123 - addProperty(o, k, currentProp, cache);
121 + logger("FORGET_CHANGE_PROP_IMMUT", source, k);
122 + addProperty(o, source, k, currentProp, cache);
123 }
124 }
125 }
@@ -128,10 +127,17 @@ function buildMakeReadOnly(
127 Object.getOwnPropertyDescriptors(o)
128 )) {
129 if (!cache.has(k) && isWriteable(prop)) {
131 - if (isInROMode() && existed) {
132 - logger("FORGET_ADD_PROP_IMMUT", k);
130 + if (prop.hasOwnProperty("set") || prop.hasOwnProperty("get") || k === "current") {
131 + // - we currently don't handle accessor properties
132 + // - we currently have no other way of checking whether an object
133 + // is a `ref` (i.e. returned by useRef).
134 + continue;
135 + }
136 +
137 + if (existed) {
138 + logger("FORGET_ADD_PROP_IMMUT", source, k);
139 }
134 - addProperty(o, k, prop, cache);
140 + addProperty(o, source, k, prop, cache);
141 }
142 }
143 return o;