10
CompilerDiagnostic,
11
CompilerError,
12
CompilerSuggestionOperation,
13
+ Effect,
14
SourceLocation,
15
} from '..';
16
import {CompilerSuggestion, ErrorCategory} from '../CompilerError';
19
BlockId,
20
DependencyPath,
21
FinishMemoize,
22
+ GeneratedSource,
23
HIRFunction,
24
Identifier,
25
IdentifierId,
26
InstructionKind,
27
+ isEffectEventFunctionType,
28
isPrimitiveType,
29
isStableType,
30
isSubPath,
43
} from '../HIR/visitors';
44
import {Result} from '../Utils/Result';
45
import {retainWhere} from '../Utils/utils';
46
+import {isEffectHook} from './ValidateMemoizedEffectDependencies';
47
48
const DEBUG = false;
49
89
export function validateExhaustiveDependencies(
90
fn: HIRFunction,
91
): Result<void, CompilerError> {
92
+ const env = fn.env;
93
const reactive = collectReactiveIdentifiersHIR(fn);
94
95
const temporaries: Map<IdentifierId, Temporary> = new Map();
131
loc: value.loc,
132
},
133
);
129
- visitCandidateDependency(value.decl, temporaries, dependencies, locals);
130
- const inferred: Array<InferredDependency> = Array.from(dependencies);
131
- // Sort dependencies by name and path, with shorter/non-optional paths first
132
- inferred.sort((a, b) => {
133
- if (a.kind === 'Global' && b.kind == 'Global') {
134
- return a.binding.name.localeCompare(b.binding.name);
135
- } else if (a.kind == 'Local' && b.kind == 'Local') {
136
- CompilerError.simpleInvariant(
137
- a.identifier.name != null &&
138
- a.identifier.name.kind === 'named' &&
139
- b.identifier.name != null &&
140
- b.identifier.name.kind === 'named',
141
- {
142
- reason: 'Expected dependencies to be named variables',
143
- loc: a.loc,
144
- },
145
- );
146
- if (a.identifier.id !== b.identifier.id) {
147
- return a.identifier.name.value.localeCompare(b.identifier.name.value);
134
+ if (env.config.validateExhaustiveMemoizationDependencies) {
135
+ visitCandidateDependency(value.decl, temporaries, dependencies, locals);
136
+ const inferred: Array<InferredDependency> = Array.from(dependencies);
137
+
138
+ const diagnostic = validateDependencies(
139
+ inferred,
140
+ startMemo.deps ?? [],
141
+ reactive,
142
+ startMemo.depsLoc,
143
+ ErrorCategory.MemoDependencies,
144
+ );
145
+ if (diagnostic != null) {
146
+ error.pushDiagnostic(diagnostic);
147
+ }
148
+ }
149
+
150
+ dependencies.clear();
151
+ locals.clear();
152
+ startMemo = null;
153
+ }
154
+
155
+ collectDependencies(
156
+ fn,
157
+ temporaries,
158
+ {
159
+ onStartMemoize,
160
+ onFinishMemoize,
161
+ onEffect: (inferred, manual, manualMemoLoc) => {
162
+ if (env.config.validateExhaustiveEffectDependencies === false) {
163
+ return;
164
}
149
- if (a.path.length !== b.path.length) {
150
- // if a's path is shorter this returns a negative, sorting a first
151
- return a.path.length - b.path.length;
165
+ if (DEBUG) {
166
+ console.log(Array.from(inferred, printInferredDependency));
167
+ console.log(Array.from(manual, printInferredDependency));
168
}
153
- for (let i = 0; i < a.path.length; i++) {
154
- const aProperty = a.path[i];
155
- const bProperty = b.path[i];
156
- const aOptional = aProperty.optional ? 0 : 1;
157
- const bOptional = bProperty.optional ? 0 : 1;
158
- if (aOptional !== bOptional) {
159
- // sort non-optionals first
160
- return aOptional - bOptional;
161
- } else if (aProperty.property !== bProperty.property) {
162
- return String(aProperty.property).localeCompare(
163
- String(bProperty.property),
164
- );
169
+ const manualDeps: Array<ManualMemoDependency> = [];
170
+ for (const dep of manual) {
171
+ if (dep.kind === 'Local') {
172
+ manualDeps.push({
173
+ root: {
174
+ kind: 'NamedLocal',
175
+ constant: false,
176
+ value: {
177
+ effect: Effect.Read,
178
+ identifier: dep.identifier,
179
+ kind: 'Identifier',
180
+ loc: dep.loc,
181
+ reactive: reactive.has(dep.identifier.id),
182
+ },
183
+ },
184
+ path: dep.path,
185
+ loc: dep.loc,
186
+ });
187
+ } else {
188
+ manualDeps.push({
189
+ root: {
190
+ kind: 'Global',
191
+ identifierName: dep.binding.name,
192
+ },
193
+ path: [],
194
+ loc: GeneratedSource,
195
+ });
196
}
197
}
167
- return 0;
168
- } else {
169
- const aName =
170
- a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
171
- const bName =
172
- b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
173
- if (aName != null && bName != null) {
174
- return aName.localeCompare(bName);
175
- }
176
- return 0;
177
- }
178
- });
179
- // remove redundant inferred dependencies
180
- retainWhere(inferred, (dep, ix) => {
181
- const match = inferred.findIndex(prevDep => {
182
- return (
183
- isEqualTemporary(prevDep, dep) ||
184
- (prevDep.kind === 'Local' &&
185
- dep.kind === 'Local' &&
186
- prevDep.identifier.id === dep.identifier.id &&
187
- isSubPath(prevDep.path, dep.path))
198
+ const diagnostic = validateDependencies(
199
+ Array.from(inferred),
200
+ manualDeps,
201
+ reactive,
202
+ manualMemoLoc,
203
+ ErrorCategory.EffectExhaustiveDependencies,
204
);
189
- });
190
- // only retain entries that don't have a prior match
191
- return match === -1 || match >= ix;
192
- });
193
- // Validate that all manual dependencies belong there
194
- if (DEBUG) {
195
- console.log('manual');
196
- console.log(
197
- (startMemo.deps ?? [])
198
- .map(x => ' ' + printManualMemoDependency(x))
199
- .join('\n'),
200
- );
201
- console.log('inferred');
202
- console.log(
203
- inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
205
+ if (diagnostic != null) {
206
+ error.pushDiagnostic(diagnostic);
207
+ }
208
+ },
209
+ },
210
+ false, // isFunctionExpression
211
+ );
212
+ return error.asResult();
213
+}
214
+
215
+function validateDependencies(
216
+ inferred: Array<InferredDependency>,
217
+ manualDependencies: Array<ManualMemoDependency>,
218
+ reactive: Set<IdentifierId>,
219
+ manualMemoLoc: SourceLocation | null,
220
+ category:
221
+ | ErrorCategory.MemoDependencies
222
+ | ErrorCategory.EffectExhaustiveDependencies,
223
+): CompilerDiagnostic | null {
224
+ // Sort dependencies by name and path, with shorter/non-optional paths first
225
+ inferred.sort((a, b) => {
226
+ if (a.kind === 'Global' && b.kind == 'Global') {
227
+ return a.binding.name.localeCompare(b.binding.name);
228
+ } else if (a.kind == 'Local' && b.kind == 'Local') {
229
+ CompilerError.simpleInvariant(
230
+ a.identifier.name != null &&
231
+ a.identifier.name.kind === 'named' &&
232
+ b.identifier.name != null &&
233
+ b.identifier.name.kind === 'named',
234
+ {
235
+ reason: 'Expected dependencies to be named variables',
236
+ loc: a.loc,
237
+ },
238
);
205
- }
206
- const manualDependencies = startMemo.deps ?? [];
207
- const matched: Set<ManualMemoDependency> = new Set();
208
- const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
209
- const extra: Array<ManualMemoDependency> = [];
210
- for (const inferredDependency of inferred) {
211
- if (inferredDependency.kind === 'Global') {
212
- for (const manualDependency of manualDependencies) {
213
- if (
214
- manualDependency.root.kind === 'Global' &&
215
- manualDependency.root.identifierName ===
216
- inferredDependency.binding.name
217
- ) {
218
- matched.add(manualDependency);
219
- extra.push(manualDependency);
220
- }
239
+ if (a.identifier.id !== b.identifier.id) {
240
+ return a.identifier.name.value.localeCompare(b.identifier.name.value);
241
+ }
242
+ if (a.path.length !== b.path.length) {
243
+ // if a's path is shorter this returns a negative, sorting a first
244
+ return a.path.length - b.path.length;
245
+ }
246
+ for (let i = 0; i < a.path.length; i++) {
247
+ const aProperty = a.path[i];
248
+ const bProperty = b.path[i];
249
+ const aOptional = aProperty.optional ? 0 : 1;
250
+ const bOptional = bProperty.optional ? 0 : 1;
251
+ if (aOptional !== bOptional) {
252
+ // sort non-optionals first
253
+ return aOptional - bOptional;
254
+ } else if (aProperty.property !== bProperty.property) {
255
+ return String(aProperty.property).localeCompare(
256
+ String(bProperty.property),
257
+ );
258
}
222
- continue;
259
}
224
- CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
225
- reason: 'Unexpected function dependency',
226
- loc: value.loc,
227
- });
228
- let hasMatchingManualDependency = false;
260
+ return 0;
261
+ } else {
262
+ const aName =
263
+ a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;
264
+ const bName =
265
+ b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;
266
+ if (aName != null && bName != null) {
267
+ return aName.localeCompare(bName);
268
+ }
269
+ return 0;
270
+ }
271
+ });
272
+ // remove redundant inferred dependencies
273
+ retainWhere(inferred, (dep, ix) => {
274
+ const match = inferred.findIndex(prevDep => {
275
+ return (
276
+ isEqualTemporary(prevDep, dep) ||
277
+ (prevDep.kind === 'Local' &&
278
+ dep.kind === 'Local' &&
279
+ prevDep.identifier.id === dep.identifier.id &&
280
+ isSubPath(prevDep.path, dep.path))
281
+ );
282
+ });
283
+ // only retain entries that don't have a prior match
284
+ return match === -1 || match >= ix;
285
+ });
286
+ // Validate that all manual dependencies belong there
287
+ if (DEBUG) {
288
+ console.log('manual');
289
+ console.log(
290
+ manualDependencies
291
+ .map(x => ' ' + printManualMemoDependency(x))
292
+ .join('\n'),
293
+ );
294
+ console.log('inferred');
295
+ console.log(
296
+ inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),
297
+ );
298
+ }
299
+ const matched: Set<ManualMemoDependency> = new Set();
300
+ const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];
301
+ const extra: Array<ManualMemoDependency> = [];
302
+ for (const inferredDependency of inferred) {
303
+ if (inferredDependency.kind === 'Global') {
304
for (const manualDependency of manualDependencies) {
305
if (
231
- manualDependency.root.kind === 'NamedLocal' &&
232
- manualDependency.root.value.identifier.id ===
233
- inferredDependency.identifier.id &&
234
- (areEqualPaths(manualDependency.path, inferredDependency.path) ||
235
- isSubPathIgnoringOptionals(
236
- manualDependency.path,
237
- inferredDependency.path,
238
- ))
306
+ manualDependency.root.kind === 'Global' &&
307
+ manualDependency.root.identifierName ===
308
+ inferredDependency.binding.name
309
) {
240
- hasMatchingManualDependency = true;
310
matched.add(manualDependency);
311
+ extra.push(manualDependency);
312
}
313
}
314
+ continue;
315
+ }
316
+ CompilerError.simpleInvariant(inferredDependency.kind === 'Local', {
317
+ reason: 'Unexpected function dependency',
318
+ loc: inferredDependency.loc,
319
+ });
320
+ /**
321
+ * Skip effect event functions as they are not valid dependencies
322
+ */
323
+ if (isEffectEventFunctionType(inferredDependency.identifier)) {
324
+ continue;
325
+ }
326
+ let hasMatchingManualDependency = false;
327
+ for (const manualDependency of manualDependencies) {
328
if (
245
- hasMatchingManualDependency ||
246
- isOptionalDependency(inferredDependency, reactive)
329
+ manualDependency.root.kind === 'NamedLocal' &&
330
+ manualDependency.root.value.identifier.id ===
331
+ inferredDependency.identifier.id &&
332
+ (areEqualPaths(manualDependency.path, inferredDependency.path) ||
333
+ isSubPathIgnoringOptionals(
334
+ manualDependency.path,
335
+ inferredDependency.path,
336
+ ))
337
) {
248
- continue;
338
+ hasMatchingManualDependency = true;
339
+ matched.add(manualDependency);
340
}
250
- missing.push(inferredDependency);
341
+ }
342
+ if (
343
+ hasMatchingManualDependency ||
344
+ isOptionalDependency(inferredDependency, reactive)
345
+ ) {
346
+ continue;
347
}
348
253
- for (const dep of startMemo.deps ?? []) {
254
- if (matched.has(dep)) {
255
- continue;
256
- }
257
- if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
258
- CompilerError.simpleInvariant(
259
- !dep.root.value.reactive &&
260
- isPrimitiveType(dep.root.value.identifier),
261
- {
262
- reason: 'Expected constant-folded dependency to be non-reactive',
263
- loc: dep.root.value.loc,
264
- },
265
- );
266
- /*
267
- * Constant primitives can get constant-folded, which means we won't
268
- * see a LoadLocal for the value within the memo function.
269
- */
270
- continue;
271
- }
272
- extra.push(dep);
349
+ missing.push(inferredDependency);
350
+ }
351
+
352
+ for (const dep of manualDependencies) {
353
+ if (matched.has(dep)) {
354
+ continue;
355
+ }
356
+ if (dep.root.kind === 'NamedLocal' && dep.root.constant) {
357
+ CompilerError.simpleInvariant(
358
+ !dep.root.value.reactive && isPrimitiveType(dep.root.value.identifier),
359
+ {
360
+ reason: 'Expected constant-folded dependency to be non-reactive',
361
+ loc: dep.root.value.loc,
362
+ },
363
+ );
364
+ /*
365
+ * Constant primitives can get constant-folded, which means we won't
366
+ * see a LoadLocal for the value within the memo function.
367
+ */
368
+ continue;
369
}
370
+ extra.push(dep);
371
+ }
372
275
- if (missing.length !== 0 || extra.length !== 0) {
276
- let suggestion: CompilerSuggestion | null = null;
277
- if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
278
- suggestion = {
279
- description: 'Update dependencies',
280
- range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
281
- op: CompilerSuggestionOperation.Replace,
282
- text: `[${inferred
283
- .filter(
284
- dep =>
285
- dep.kind === 'Local' && !isOptionalDependency(dep, reactive),
286
- )
287
- .map(printInferredDependency)
288
- .join(', ')}]`,
289
- };
373
+ if (missing.length !== 0 || extra.length !== 0) {
374
+ let suggestion: CompilerSuggestion | null = null;
375
+ if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') {
376
+ suggestion = {
377
+ description: 'Update dependencies',
378
+ range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
379
+ op: CompilerSuggestionOperation.Replace,
380
+ text: `[${inferred
381
+ .filter(
382
+ dep =>
383
+ dep.kind === 'Local' &&
384
+ !isOptionalDependency(dep, reactive) &&
385
+ !isEffectEventFunctionType(dep.identifier),
386
+ )
387
+ .map(printInferredDependency)
388
+ .join(', ')}]`,
389
+ };
390
+ }
391
+ const diagnostic = createDiagnostic(category, missing, extra, suggestion);
392
+ for (const dep of missing) {
393
+ let reactiveStableValueHint = '';
394
+ if (isStableType(dep.identifier)) {
395
+ reactiveStableValueHint =
396
+ '. Refs, setState functions, and other "stable" values generally do not need to be added ' +
397
+ 'as dependencies, but this variable may change over time to point to different values';
398
}
291
- const diagnostic = CompilerDiagnostic.create({
292
- category: ErrorCategory.MemoDependencies,
293
- reason: 'Found missing/extra memoization dependencies',
294
- description: [
295
- missing.length !== 0
296
- ? 'Missing dependencies can cause a value to update less often than it should, ' +
297
- 'resulting in stale UI'
298
- : null,
299
- extra.length !== 0
300
- ? 'Extra dependencies can cause a value to update more often than it should, ' +
301
- 'resulting in performance problems such as excessive renders or effects firing too often'
302
- : null,
303
- ]
304
- .filter(Boolean)
305
- .join('. '),
306
- suggestions: suggestion != null ? [suggestion] : null,
399
+ diagnostic.withDetails({
400
+ kind: 'error',
401
+ message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
402
+ loc: dep.loc,
403
});
308
- for (const dep of missing) {
309
- let reactiveStableValueHint = '';
310
- if (isStableType(dep.identifier)) {
311
- reactiveStableValueHint =
312
- '. Refs, setState functions, and other "stable" values generally do not need to be added ' +
313
- 'as dependencies, but this variable may change over time to point to different values';
314
- }
404
+ }
405
+ for (const dep of extra) {
406
+ if (dep.root.kind === 'Global') {
407
diagnostic.withDetails({
408
kind: 'error',
317
- message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
318
- loc: dep.loc,
409
+ message:
410
+ `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
411
+ 'Values declared outside of a component/hook should not be listed as ' +
412
+ 'dependencies as the component will not re-render if they change',
413
+ loc: dep.loc ?? manualMemoLoc,
414
});
320
- }
321
- for (const dep of extra) {
322
- if (dep.root.kind === 'Global') {
415
+ } else {
416
+ const root = dep.root.value;
417
+ const matchingInferred = inferred.find(
418
+ (
419
+ inferredDep,
420
+ ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
421
+ return (
422
+ inferredDep.kind === 'Local' &&
423
+ inferredDep.identifier.id === root.identifier.id &&
424
+ isSubPathIgnoringOptionals(inferredDep.path, dep.path)
425
+ );
426
+ },
427
+ );
428
+ if (
429
+ matchingInferred != null &&
430
+ isEffectEventFunctionType(matchingInferred.identifier)
431
+ ) {
432
diagnostic.withDetails({
433
kind: 'error',
434
message:
326
- `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
327
- 'Values declared outside of a component/hook should not be listed as ' +
328
- 'dependencies as the component will not re-render if they change',
329
- loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
435
+ `Functions returned from \`useEffectEvent\` must not be included in the dependency array. ` +
436
+ `Remove \`${printManualMemoDependency(dep)}\` from the dependencies.`,
437
+ loc: dep.loc ?? manualMemoLoc,
438
+ });
439
+ } else if (
440
+ matchingInferred != null &&
441
+ !isOptionalDependency(matchingInferred, reactive)
442
+ ) {
443
+ diagnostic.withDetails({
444
+ kind: 'error',
445
+ message:
446
+ `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
447
+ `use \`${printInferredDependency(matchingInferred)}\` instead`,
448
+ loc: dep.loc ?? manualMemoLoc,
449
});
331
- error.pushDiagnostic(diagnostic);
450
} else {
333
- const root = dep.root.value;
334
- const matchingInferred = inferred.find(
335
- (
336
- inferredDep,
337
- ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
338
- return (
339
- inferredDep.kind === 'Local' &&
340
- inferredDep.identifier.id === root.identifier.id &&
341
- isSubPathIgnoringOptionals(inferredDep.path, dep.path)
342
- );
343
- },
344
- );
345
- if (
346
- matchingInferred != null &&
347
- !isOptionalDependency(matchingInferred, reactive)
348
- ) {
349
- diagnostic.withDetails({
350
- kind: 'error',
351
- message:
352
- `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
353
- `use \`${printInferredDependency(matchingInferred)}\` instead`,
354
- loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
355
- });
356
- } else {
357
- /**
358
- * Else this dependency doesn't correspond to anything referenced in the memo function,
359
- * or is an optional dependency so we don't want to suggest adding it
360
- */
361
- diagnostic.withDetails({
362
- kind: 'error',
363
- message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
364
- loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
365
- });
366
- }
451
+ /**
452
+ * Else this dependency doesn't correspond to anything referenced in the memo function,
453
+ * or is an optional dependency so we don't want to suggest adding it
454
+ */
455
+ diagnostic.withDetails({
456
+ kind: 'error',
457
+ message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
458
+ loc: dep.loc ?? manualMemoLoc,
459
+ });
460
}
461
}
369
- if (suggestion != null) {
370
- diagnostic.withDetails({
371
- kind: 'hint',
372
- message: `Inferred dependencies: \`${suggestion.text}\``,
373
- });
374
- }
375
- error.pushDiagnostic(diagnostic);
462
}
377
-
378
- dependencies.clear();
379
- locals.clear();
380
- startMemo = null;
463
+ if (suggestion != null) {
464
+ diagnostic.withDetails({
465
+ kind: 'hint',
466
+ message: `Inferred dependencies: \`${suggestion.text}\``,
467
+ });
468
+ }
469
+ return diagnostic;
470
}
382
-
383
- collectDependencies(
384
- fn,
385
- temporaries,
386
- {
387
- onStartMemoize,
388
- onFinishMemoize,
389
- },
390
- false, // isFunctionExpression
391
- );
392
- return error.asResult();
471
+ return null;
472
}
473
474
function addDependency(
476
dependencies: Set<InferredDependency>,
477
locals: Set<IdentifierId>,
478
): void {
400
- if (dep.kind === 'Function') {
479
+ if (dep.kind === 'Aggregate') {
480
for (const x of dep.dependencies) {
481
addDependency(x, dependencies, locals);
482
}
559
dependencies: Set<InferredDependency>,
560
locals: Set<IdentifierId>,
561
) => void;
562
+ onEffect: (
563
+ inferred: Set<InferredDependency>,
564
+ manual: Set<InferredDependency>,
565
+ manualMemoLoc: SourceLocation | null,
566
+ ) => void;
567
} | null,
568
isFunctionExpression: boolean,
485
-): Extract<Temporary, {kind: 'Function'}> {
569
+): Extract<Temporary, {kind: 'Aggregate'}> {
570
const optionals = findOptionalPlaces(fn);
571
if (DEBUG) {
572
console.log(prettyFormat(optionals));
585
}
586
for (const block of fn.body.blocks.values()) {
587
for (const phi of block.phis) {
504
- let deps: Array<Temporary> | null = null;
588
+ const deps: Array<InferredDependency> = [];
589
for (const operand of phi.operands.values()) {
590
const dep = temporaries.get(operand.identifier.id);
591
if (dep == null) {
592
continue;
593
}
510
- if (deps == null) {
511
- deps = [dep];
594
+ if (dep.kind === 'Aggregate') {
595
+ deps.push(...dep.dependencies);
596
} else {
597
deps.push(dep);
598
}
599
}
516
- if (deps == null) {
600
+ if (deps.length === 0) {
601
continue;
602
} else if (deps.length === 1) {
603
temporaries.set(phi.place.identifier.id, deps[0]!);
604
} else {
605
temporaries.set(phi.place.identifier.id, {
522
- kind: 'Function',
606
+ kind: 'Aggregate',
607
dependencies: new Set(deps),
608
});
609
}
621
}
622
case 'LoadContext':
623
case 'LoadLocal': {
540
- if (locals.has(value.place.identifier.id)) {
541
- break;
542
- }
624
const temp = temporaries.get(value.place.identifier.id);
625
if (temp != null) {
626
if (temp.kind === 'Local') {
629
} else {
630
temporaries.set(lvalue.identifier.id, temp);
631
}
632
+ if (locals.has(value.place.identifier.id)) {
633
+ locals.add(lvalue.identifier.id);
634
+ }
635
}
636
break;
637
}
767
}
768
break;
769
}
770
+ case 'ArrayExpression': {
771
+ const arrayDeps: Set<InferredDependency> = new Set();
772
+ for (const item of value.elements) {
773
+ if (item.kind === 'Hole') {
774
+ continue;
775
+ }
776
+ const place = item.kind === 'Identifier' ? item : item.place;
777
+ // Visit with alternative deps/locals to record manual dependencies
778
+ visitCandidateDependency(place, temporaries, arrayDeps, new Set());
779
+ // Visit normally to propagate inferred dependencies upward
780
+ visit(place);
781
+ }
782
+ temporaries.set(lvalue.identifier.id, {
783
+ kind: 'Aggregate',
784
+ dependencies: arrayDeps,
785
+ loc: value.loc,
786
+ });
787
+ break;
788
+ }
789
+ case 'CallExpression':
790
case 'MethodCall': {
791
+ const receiver =
792
+ value.kind === 'CallExpression' ? value.callee : value.property;
793
+
794
+ const onEffect = callbacks?.onEffect;
795
+ if (onEffect != null && isEffectHook(receiver.identifier)) {
796
+ const [fn, deps] = value.args;
797
+ if (fn?.kind === 'Identifier' && deps?.kind === 'Identifier') {
798
+ const fnDeps = temporaries.get(fn.identifier.id);
799
+ const manualDeps = temporaries.get(deps.identifier.id);
800
+ if (
801
+ fnDeps?.kind === 'Aggregate' &&
802
+ manualDeps?.kind === 'Aggregate'
803
+ ) {
804
+ onEffect(
805
+ fnDeps.dependencies,
806
+ manualDeps.dependencies,
807
+ manualDeps.loc ?? null,
808
+ );
809
+ }
810
+ }
811
+ }
812
+
813
// Ignore the method itself
814
for (const operand of eachInstructionValueOperand(value)) {
689
- if (operand.identifier.id === value.property.identifier.id) {
815
+ if (
816
+ value.kind === 'MethodCall' &&
817
+ operand.identifier.id === value.property.identifier.id
818
+ ) {
819
continue;
820
}
821
visit(operand);
839
visit(operand);
840
}
841
}
713
- return {kind: 'Function', dependencies};
842
+ return {kind: 'Aggregate', dependencies};
843
}
844
845
function printInferredDependency(dep: InferredDependency): string {
877
878
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
879
switch (a.kind) {
751
- case 'Function': {
880
+ case 'Aggregate': {
881
return false;
882
}
883
case 'Global': {
902
context: boolean;
903
loc: SourceLocation;
904
}
776
- | {kind: 'Function'; dependencies: Set<Temporary>};
905
+ | {
906
+ kind: 'Aggregate';
907
+ dependencies: Set<InferredDependency>;
908
+ loc?: SourceLocation;
909
+ };
910
type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;
911
912
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
1021
isPrimitiveType(inferredDependency.identifier))
1022
);
1023
}
1024
+
1025
+function createDiagnostic(
1026
+ category:
1027
+ | ErrorCategory.MemoDependencies
1028
+ | ErrorCategory.EffectExhaustiveDependencies,
1029
+ missing: Array<InferredDependency>,
1030
+ extra: Array<ManualMemoDependency>,
1031
+ suggestion: CompilerSuggestion | null,
1032
+): CompilerDiagnostic {
1033
+ let reason: string;
1034
+ let description: string;
1035
+
1036
+ function joinMissingExtraDetail(
1037
+ missingString: string,
1038
+ extraString: string,
1039
+ joinStr: string,
1040
+ ): string {
1041
+ return [
1042
+ missing.length !== 0 ? missingString : null,
1043
+ extra.length !== 0 ? extraString : null,
1044
+ ]
1045
+ .filter(Boolean)
1046
+ .join(joinStr);
1047
+ }
1048
+
1049
+ switch (category) {
1050
+ case ErrorCategory.MemoDependencies: {
1051
+ reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} memoization dependencies`;
1052
+ description = joinMissingExtraDetail(
1053
+ 'Missing dependencies can cause a value to update less often than it should, resulting in stale UI',
1054
+ 'Extra dependencies can cause a value to update more often than it should, resulting in performance' +
1055
+ ' problems such as excessive renders or effects firing too often',
1056
+ '. ',
1057
+ );
1058
+ break;
1059
+ }
1060
+ case ErrorCategory.EffectExhaustiveDependencies: {
1061
+ reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} effect dependencies`;
1062
+ description = joinMissingExtraDetail(
1063
+ 'Missing dependencies can cause an effect to fire less often than it should',
1064
+ 'Extra dependencies can cause an effect to fire more often than it should, resulting' +
1065
+ ' in performance problems such as excessive renders and side effects',
1066
+ '. ',
1067
+ );
1068
+ break;
1069
+ }
1070
+ default: {
1071
+ CompilerError.simpleInvariant(false, {
1072
+ reason: `Unexpected error category: ${category}`,
1073
+ loc: GeneratedSource,
1074
+ });
1075
+ }
1076
+ }
1077
+
1078
+ return CompilerDiagnostic.create({
1079
+ category,
1080
+ reason,
1081
+ description,
1082
+ suggestions: suggestion != null ? [suggestion] : null,
1083
+ });
1084
+}