43
const DEBUG = false;
44
45
/**
46
- * Validates that existing manual memoization had exhaustive dependencies.
47
- * Memoization with missing or extra reactive dependencies is invalid React
48
- * and compilation can change behavior, causing a value to be computed more
49
- * or less times.
46
+ * Validates that existing manual memoization is exhaustive and does not
47
+ * have extraneous dependencies. The goal of the validation is to ensure
48
+ * that auto-memoization will not substantially change the behavior of
49
+ * the program:
50
+ * - If the manual dependencies were non-exhaustive (missing important deps)
51
+ * then auto-memoization will include those dependencies, and cause the
52
+ * value to update *more* frequently.
53
+ * - If the manual dependencies had extraneous deps, then auto memoization
54
+ * will remove them and cause the value to update *less* frequently.
55
*
51
- * TODOs:
52
- * - Handle cases of mixed optional and non-optional versions of the same path,
53
- * eg referecing both x.y.z and x.y?.z in the same memo block. we should collapse
54
- * this into a single canonical dep that we look for in the manual deps. see the
55
- * existing exhaustive deps rule for implementation.
56
- * - Handle cases where the user deps were not simple identifiers + property chains.
57
- * We try to detect this in ValidateUseMemo but we miss some cases. The problem
58
- * is that invalid forms can be value blocks or function calls that don't get
59
- * removed by DCE, leaving a structure like:
56
+ * We consider a value V as missing if ALL of the following conditions are met:
57
+ * - V is reactive
58
+ * - There is no manual dependency path P such that whenever V would change,
59
+ * P would also change. If V is `x.y.z`, this means there must be some
60
+ * path P that is either `x.y.z`, `x.y`, or `x`. Note that we assume no
61
+ * interior mutability, such that a shorter path "covers" changes to longer
62
+ * more precise paths.
63
+ *
64
+ * We consider a value V extraneous if either of the folowing are true:
65
+ * - V is a reactive local that is unreferenced
66
+ * - V is a global that is unreferenced
67
+ *
68
+ * In other words, we allow extraneous non-reactive values since we know they cannot
69
+ * impact how often the memoization would run.
70
+ *
71
+ * ## TODO: Invalid, Complex Deps
72
+ *
73
+ * Handle cases where the user deps were not simple identifiers + property chains.
74
+ * We try to detect this in ValidateUseMemo but we miss some cases. The problem
75
+ * is that invalid forms can be value blocks or function calls that don't get
76
+ * removed by DCE, leaving a structure like:
77
*
78
* StartMemoize
79
* t0 = <value to memoize>
226
reason: 'Unexpected function dependency',
227
loc: value.loc,
228
});
212
- /**
213
- * Dependencies technically only need to include reactive values. However,
214
- * reactivity inference for general values is subtle since it involves all
215
- * of our complex control and data flow analysis. To keep results more
216
- * stable and predictable to developers, we intentionally stay closer to
217
- * the rules of the classic exhaustive-deps rule. Values should be included
218
- * as dependencies if either of the following is true:
219
- * - They're reactive
220
- * - They're non-reactive and not a known-stable value type.
221
- *
222
- * Thus `const ref: Ref = cond ? ref1 : ref2` has to be a dependency
223
- * (assuming `cond` is reactive) since it's reactive despite being a ref.
224
- *
225
- * Similarly, `const x = [1,2,3]` has to be a dependency since even
226
- * though it's non reactive, it's not a known stable type.
227
- *
228
- * TODO: consider reimplementing a simpler form of reactivity inference.
229
- * Ideally we'd consider `const ref: Ref = cond ? ref1 : ref2` as a required
230
- * dependency even if our data/control flow tells us that `cond` is non-reactive.
231
- * It's simpler for developers to reason about based on a more structural/AST
232
- * driven approach.
233
- */
234
- const isRequiredDependency =
235
- reactive.has(inferredDependency.identifier.id) ||
236
- !isStableType(inferredDependency.identifier);
229
+ const isRequiredDependency = reactive.has(
230
+ inferredDependency.identifier.id,
231
+ );
232
let hasMatchingManualDependency = false;
233
for (const manualDependency of manualDependencies) {
234
if (
260
extra.push(dep);
261
}
262
268
- /*
269
- * For compatiblity with the existing exhaustive-deps rule, we allow
270
- * known-stable values as dependencies even if the value is not reactive.
271
- * This allows code that takes a dep on a non-reactive setState function
272
- * to pass, for example.
263
+ /**
264
+ * Per docblock, we only consider dependencies as extraneous if
265
+ * they are unused globals or reactive locals. Notably, this allows
266
+ * non-reactive locals.
267
*/
268
retainWhere(extra, dep => {
275
- const isNonReactiveStableValue =
276
- dep.root.kind === 'NamedLocal' &&
277
- !dep.root.value.reactive &&
278
- isStableType(dep.root.value.identifier);
279
- return !isNonReactiveStableValue;
269
+ return dep.root.kind === 'Global' || dep.root.value.reactive;
270
});
271
272
if (missing.length !== 0 || extra.length !== 0) {