1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import prettyFormat from 'pretty-format';9import {10 CompilerDiagnostic,11 CompilerError,12 CompilerSuggestionOperation,13 Effect,14 SourceLocation,15} from '..';16import {CompilerSuggestion, ErrorCategory} from '../CompilerError';17import {18 areEqualPaths,19 BlockId,20 DependencyPath,21 FinishMemoize,22 GeneratedSource,23 HIRFunction,24 Identifier,25 IdentifierId,26 InstructionKind,27 isEffectEventFunctionType,28 isPrimitiveType,29 isStableType,30 isSubPath,31 isSubPathIgnoringOptionals,32 isUseEffectHookType,33 isUseInsertionEffectHookType,34 isUseLayoutEffectHookType,35 isUseRefType,36 LoadGlobal,37 ManualMemoDependency,38 Place,39 StartMemoize,40} from '../HIR';41import {42 eachInstructionLValue,43 eachInstructionValueLValue,44 eachInstructionValueOperand,45 eachTerminalOperand,46} from '../HIR/visitors';47import {retainWhere} from '../Utils/utils';4849const DEBUG = false;5051/**52 * Validates that existing manual memoization is exhaustive and does not53 * have extraneous dependencies. The goal of the validation is to ensure54 * that auto-memoization will not substantially change the behavior of55 * the program:56 * - If the manual dependencies were non-exhaustive (missing important deps)57 * then auto-memoization will include those dependencies, and cause the58 * value to update *more* frequently.59 * - If the manual dependencies had extraneous deps, then auto memoization60 * will remove them and cause the value to update *less* frequently.61 *62 * The implementation compares the manual dependencies against the values63 * actually used within the memoization function64 * - For each value V referenced in the memo function, either:65 * - If the value is non-reactive *and* a known stable type, then the66 * value may optionally be specified as an exact dependency.67 * - Otherwise, report an error unless there is a manual dependency that will68 * invalidate whenever V invalidates. If `x.y.z` is referenced, there must69 * be a manual dependency for `x.y.z`, `x.y`, or `x`. Note that we assume70 * no interior mutability, ie we assume that any changes to inner paths must71 * always cause the other path to change as well.72 * - Any dependencies that do not correspond to a value referenced in the memo73 * function are considered extraneous and throw an error74 *75 * ## TODO: Invalid, Complex Deps76 *77 * Handle cases where the user deps were not simple identifiers + property chains.78 * We try to detect this in ValidateUseMemo but we miss some cases. The problem79 * is that invalid forms can be value blocks or function calls that don't get80 * removed by DCE, leaving a structure like:81 *82 * StartMemoize83 * t0 = <value to memoize>84 * ...non-DCE'd code for manual deps...85 * FinishMemoize decl=t086 *87 * When we go to compute the dependencies, we then think that the user's manual dep88 * logic is part of what the memo computation logic.89 */90export function validateExhaustiveDependencies(fn: HIRFunction): void {91 const env = fn.env;92 const reactive = collectReactiveIdentifiersHIR(fn);9394 const temporaries: Map<IdentifierId, Temporary> = new Map();95 for (const param of fn.params) {96 const place = param.kind === 'Identifier' ? param : param.place;97 temporaries.set(place.identifier.id, {98 kind: 'Local',99 identifier: place.identifier,100 path: [],101 context: false,102 loc: place.loc,103 });104 }105 let startMemo: StartMemoize | null = null;106107 function onStartMemoize(108 value: StartMemoize,109 dependencies: Set<InferredDependency>,110 locals: Set<IdentifierId>,111 ): void {112 CompilerError.invariant(startMemo == null, {113 reason: 'Unexpected nested memo calls',114 loc: value.loc,115 });116 startMemo = value;117 dependencies.clear();118 locals.clear();119 }120 function onFinishMemoize(121 value: FinishMemoize,122 dependencies: Set<InferredDependency>,123 locals: Set<IdentifierId>,124 ): void {125 CompilerError.invariant(126 startMemo != null && startMemo.manualMemoId === value.manualMemoId,127 {128 reason: 'Found FinishMemoize without corresponding StartMemoize',129 loc: value.loc,130 },131 );132 if (env.config.validateExhaustiveMemoizationDependencies) {133 visitCandidateDependency(value.decl, temporaries, dependencies, locals);134 const inferred: Array<InferredDependency> = Array.from(dependencies);135136 const diagnostic = validateDependencies(137 inferred,138 startMemo.deps ?? [],139 reactive,140 startMemo.depsLoc,141 ErrorCategory.MemoDependencies,142 'all',143 );144 if (diagnostic != null) {145 fn.env.recordError(diagnostic);146 startMemo.hasInvalidDeps = true;147 }148 }149150 dependencies.clear();151 locals.clear();152 startMemo = null;153 }154155 collectDependencies(156 fn,157 temporaries,158 {159 onStartMemoize,160 onFinishMemoize,161 onEffect: (inferred, manual, manualMemoLoc) => {162 if (env.config.validateExhaustiveEffectDependencies === 'off') {163 return;164 }165 if (DEBUG) {166 console.log(Array.from(inferred, printInferredDependency));167 console.log(Array.from(manual, printInferredDependency));168 }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 }198 const effectReportMode =199 typeof env.config.validateExhaustiveEffectDependencies === 'string'200 ? env.config.validateExhaustiveEffectDependencies201 : 'all';202 const diagnostic = validateDependencies(203 Array.from(inferred),204 manualDeps,205 reactive,206 manualMemoLoc,207 ErrorCategory.EffectExhaustiveDependencies,208 effectReportMode,209 );210 if (diagnostic != null) {211 fn.env.recordError(diagnostic);212 }213 },214 },215 false, // isFunctionExpression216 );217}218219function validateDependencies(220 inferred: Array<InferredDependency>,221 manualDependencies: Array<ManualMemoDependency>,222 reactive: Set<IdentifierId>,223 manualMemoLoc: SourceLocation | null,224 category:225 | ErrorCategory.MemoDependencies226 | ErrorCategory.EffectExhaustiveDependencies,227 exhaustiveDepsReportMode: 'all' | 'missing-only' | 'extra-only',228): CompilerDiagnostic | null {229 // Sort dependencies by name and path, with shorter/non-optional paths first230 inferred.sort((a, b) => {231 if (a.kind === 'Global' && b.kind == 'Global') {232 return a.binding.name.localeCompare(b.binding.name);233 } else if (a.kind == 'Local' && b.kind == 'Local') {234 CompilerError.invariant(235 a.identifier.name != null &&236 a.identifier.name.kind === 'named' &&237 b.identifier.name != null &&238 b.identifier.name.kind === 'named',239 {240 reason: 'Expected dependencies to be named variables',241 loc: a.loc,242 },243 );244 if (a.identifier.id !== b.identifier.id) {245 return a.identifier.name.value.localeCompare(b.identifier.name.value);246 }247 if (a.path.length !== b.path.length) {248 // if a's path is shorter this returns a negative, sorting a first249 return a.path.length - b.path.length;250 }251 for (let i = 0; i < a.path.length; i++) {252 const aProperty = a.path[i];253 const bProperty = b.path[i];254 const aOptional = aProperty.optional ? 0 : 1;255 const bOptional = bProperty.optional ? 0 : 1;256 if (aOptional !== bOptional) {257 // sort non-optionals first258 return aOptional - bOptional;259 } else if (aProperty.property !== bProperty.property) {260 return String(aProperty.property).localeCompare(261 String(bProperty.property),262 );263 }264 }265 return 0;266 } else {267 const aName =268 a.kind === 'Global' ? a.binding.name : a.identifier.name?.value;269 const bName =270 b.kind === 'Global' ? b.binding.name : b.identifier.name?.value;271 if (aName != null && bName != null) {272 return aName.localeCompare(bName);273 }274 return 0;275 }276 });277 // remove redundant inferred dependencies278 retainWhere(inferred, (dep, ix) => {279 const match = inferred.findIndex(prevDep => {280 return (281 isEqualTemporary(prevDep, dep) ||282 (prevDep.kind === 'Local' &&283 dep.kind === 'Local' &&284 prevDep.identifier.id === dep.identifier.id &&285 isSubPath(prevDep.path, dep.path))286 );287 });288 // only retain entries that don't have a prior match289 return match === -1 || match >= ix;290 });291 // Validate that all manual dependencies belong there292 if (DEBUG) {293 console.log('manual');294 console.log(295 manualDependencies296 .map(x => ' ' + printManualMemoDependency(x))297 .join('\n'),298 );299 console.log('inferred');300 console.log(301 inferred.map(x => ' ' + printInferredDependency(x)).join('\n'),302 );303 }304 const matched: Set<ManualMemoDependency> = new Set();305 const missing: Array<Extract<InferredDependency, {kind: 'Local'}>> = [];306 const extra: Array<ManualMemoDependency> = [];307 for (const inferredDependency of inferred) {308 if (inferredDependency.kind === 'Global') {309 for (const manualDependency of manualDependencies) {310 if (311 manualDependency.root.kind === 'Global' &&312 manualDependency.root.identifierName ===313 inferredDependency.binding.name314 ) {315 matched.add(manualDependency);316 extra.push(manualDependency);317 }318 }319 continue;320 }321 CompilerError.invariant(inferredDependency.kind === 'Local', {322 reason: 'Unexpected function dependency',323 loc: inferredDependency.loc,324 });325 /**326 * Skip effect event functions as they are not valid dependencies327 */328 if (isEffectEventFunctionType(inferredDependency.identifier)) {329 continue;330 }331 let hasMatchingManualDependency = false;332 for (const manualDependency of manualDependencies) {333 if (334 manualDependency.root.kind === 'NamedLocal' &&335 manualDependency.root.value.identifier.id ===336 inferredDependency.identifier.id &&337 (areEqualPaths(manualDependency.path, inferredDependency.path) ||338 isSubPathIgnoringOptionals(339 manualDependency.path,340 inferredDependency.path,341 ))342 ) {343 hasMatchingManualDependency = true;344 matched.add(manualDependency);345 }346 }347 if (348 hasMatchingManualDependency ||349 isOptionalDependency(inferredDependency, reactive)350 ) {351 continue;352 }353354 missing.push(inferredDependency);355 }356357 for (const dep of manualDependencies) {358 if (matched.has(dep)) {359 continue;360 }361 if (dep.root.kind === 'NamedLocal' && dep.root.constant) {362 CompilerError.invariant(363 !dep.root.value.reactive && isPrimitiveType(dep.root.value.identifier),364 {365 reason: 'Expected constant-folded dependency to be non-reactive',366 loc: dep.root.value.loc,367 },368 );369 /*370 * Constant primitives can get constant-folded, which means we won't371 * see a LoadLocal for the value within the memo function.372 */373 continue;374 }375 extra.push(dep);376 }377378 // Filter based on report mode379 const filteredMissing =380 exhaustiveDepsReportMode === 'extra-only' ? [] : missing;381 const filteredExtra =382 exhaustiveDepsReportMode === 'missing-only' ? [] : extra;383384 if (filteredMissing.length !== 0 || filteredExtra.length !== 0) {385 let suggestion: CompilerSuggestion | null = null;386 if (387 manualMemoLoc != null &&388 typeof manualMemoLoc !== 'symbol' &&389 manualMemoLoc.start.index != null &&390 manualMemoLoc.end.index != null391 ) {392 suggestion = {393 description: 'Update dependencies',394 range: [manualMemoLoc.start.index, manualMemoLoc.end.index],395 op: CompilerSuggestionOperation.Replace,396 text: `[${inferred397 .filter(398 dep =>399 dep.kind === 'Local' &&400 !isOptionalDependency(dep, reactive) &&401 !isEffectEventFunctionType(dep.identifier),402 )403 .map(printInferredDependency)404 .join(', ')}]`,405 };406 }407 const diagnostic = createDiagnostic(408 category,409 filteredMissing,410 filteredExtra,411 suggestion,412 );413 for (const dep of filteredMissing) {414 let reactiveStableValueHint = '';415 if (isStableType(dep.identifier)) {416 reactiveStableValueHint =417 '. Refs, setState functions, and other "stable" values generally do not need to be added ' +418 'as dependencies, but this variable may change over time to point to different values';419 }420 diagnostic.withDetails({421 kind: 'error',422 message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,423 loc: dep.loc,424 });425 }426 for (const dep of filteredExtra) {427 if (dep.root.kind === 'Global') {428 diagnostic.withDetails({429 kind: 'error',430 message:431 `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +432 'Values declared outside of a component/hook should not be listed as ' +433 'dependencies as the component will not re-render if they change',434 loc: dep.loc ?? manualMemoLoc,435 });436 } else {437 const root = dep.root.value;438 const matchingInferred = inferred.find(439 (440 inferredDep,441 ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {442 return (443 inferredDep.kind === 'Local' &&444 inferredDep.identifier.id === root.identifier.id &&445 isSubPathIgnoringOptionals(inferredDep.path, dep.path)446 );447 },448 );449 if (450 matchingInferred != null &&451 isEffectEventFunctionType(matchingInferred.identifier)452 ) {453 diagnostic.withDetails({454 kind: 'error',455 message:456 `Functions returned from \`useEffectEvent\` must not be included in the dependency array. ` +457 `Remove \`${printManualMemoDependency(dep)}\` from the dependencies.`,458 loc: dep.loc ?? manualMemoLoc,459 });460 } else if (461 matchingInferred != null &&462 !isOptionalDependency(matchingInferred, reactive)463 ) {464 diagnostic.withDetails({465 kind: 'error',466 message:467 `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +468 `use \`${printInferredDependency(matchingInferred)}\` instead`,469 loc: dep.loc ?? manualMemoLoc,470 });471 } else {472 /**473 * Else this dependency doesn't correspond to anything referenced in the memo function,474 * or is an optional dependency so we don't want to suggest adding it475 */476 diagnostic.withDetails({477 kind: 'error',478 message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,479 loc: dep.loc ?? manualMemoLoc,480 });481 }482 }483 }484 if (suggestion != null) {485 diagnostic.withDetails({486 kind: 'hint',487 message: `Inferred dependencies: \`${suggestion.text}\``,488 });489 }490 return diagnostic;491 }492 return null;493}494495function addDependency(496 dep: Temporary,497 dependencies: Set<InferredDependency>,498 locals: Set<IdentifierId>,499): void {500 if (dep.kind === 'Aggregate') {501 for (const x of dep.dependencies) {502 addDependency(x, dependencies, locals);503 }504 } else if (dep.kind === 'Global') {505 dependencies.add(dep);506 } else if (!locals.has(dep.identifier.id)) {507 dependencies.add(dep);508 }509}510511function visitCandidateDependency(512 place: Place,513 temporaries: Map<IdentifierId, Temporary>,514 dependencies: Set<InferredDependency>,515 locals: Set<IdentifierId>,516): void {517 const dep = temporaries.get(place.identifier.id);518 if (dep != null) {519 addDependency(dep, dependencies, locals);520 }521}522523/**524 * This function determines the dependencies of the given function relative to525 * its external context. Dependencies are collected eagerly, the first time an526 * external variable is referenced, as opposed to trying to delay or aggregate527 * calculation of dependencies until they are later "used".528 *529 * For example, in530 *531 * ```532 * function f() {533 * let x = y; // we record a dependency on `y` here534 * ...535 * use(x); // as opposed to trying to delay that dependency until here536 * }537 * ```538 *539 * That said, LoadLocal/LoadContext does not immediately take a dependency,540 * we store the dependency in a temporary and set it as used when that temporary541 * is referenced as an operand.542 *543 * As we proceed through the function we track local variables that it creates544 * and don't consider later references to these variables as dependencies.545 *546 * For function expressions we first collect the function's dependencies by547 * calling this function recursively, _without_ taking into account whether548 * the "external" variables it accesses are actually external or just locals549 * in the parent. We then prune any locals and immediately consider any550 * remaining externals that it accesses as a dependency:551 *552 * ```553 * function Component() {554 * const local = ...;555 * const f = () => { return [external, local] };556 * }557 * ```558 *559 * Here we calculate `f` as having dependencies `external, `local` and save560 * this into `temporaries`. We then also immediately take these as dependencies561 * at the Component scope, at which point we filter out `local` as a local variable,562 * leaving just a dependency on `external`.563 *564 * When calling this function on a top-level component or hook, the collected dependencies565 * will only contain the globals that it accesses which isn't useful. Instead, passing566 * onStartMemoize/onFinishMemoize callbacks allows looking at the dependencies within567 * blocks of manual memoization.568 */569function collectDependencies(570 fn: HIRFunction,571 temporaries: Map<IdentifierId, Temporary>,572 callbacks: {573 onStartMemoize: (574 startMemo: StartMemoize,575 dependencies: Set<InferredDependency>,576 locals: Set<IdentifierId>,577 ) => void;578 onFinishMemoize: (579 finishMemo: FinishMemoize,580 dependencies: Set<InferredDependency>,581 locals: Set<IdentifierId>,582 ) => void;583 onEffect: (584 inferred: Set<InferredDependency>,585 manual: Set<InferredDependency>,586 manualMemoLoc: SourceLocation | null,587 ) => void;588 } | null,589 isFunctionExpression: boolean,590): Extract<Temporary, {kind: 'Aggregate'}> {591 const optionals = findOptionalPlaces(fn);592 if (DEBUG) {593 console.log(prettyFormat(optionals));594 }595 const locals: Set<IdentifierId> = new Set();596 if (isFunctionExpression) {597 for (const param of fn.params) {598 const place = param.kind === 'Identifier' ? param : param.place;599 locals.add(place.identifier.id);600 }601 }602603 const dependencies: Set<InferredDependency> = new Set();604 function visit(place: Place): void {605 visitCandidateDependency(place, temporaries, dependencies, locals);606 }607 for (const block of fn.body.blocks.values()) {608 for (const phi of block.phis) {609 const deps: Array<InferredDependency> = [];610 for (const operand of phi.operands.values()) {611 const dep = temporaries.get(operand.identifier.id);612 if (dep == null) {613 continue;614 }615 if (dep.kind === 'Aggregate') {616 deps.push(...dep.dependencies);617 } else {618 deps.push(dep);619 }620 }621 if (deps.length === 0) {622 continue;623 } else if (deps.length === 1) {624 temporaries.set(phi.place.identifier.id, deps[0]!);625 } else {626 temporaries.set(phi.place.identifier.id, {627 kind: 'Aggregate',628 dependencies: new Set(deps),629 });630 }631 }632633 for (const instr of block.instructions) {634 const {lvalue, value} = instr;635 switch (value.kind) {636 case 'LoadGlobal': {637 temporaries.set(lvalue.identifier.id, {638 kind: 'Global',639 binding: value.binding,640 });641 break;642 }643 case 'LoadContext':644 case 'LoadLocal': {645 const temp = temporaries.get(value.place.identifier.id);646 if (temp != null) {647 if (temp.kind === 'Local') {648 const local: Temporary = {...temp, loc: value.place.loc};649 temporaries.set(lvalue.identifier.id, local);650 } else {651 temporaries.set(lvalue.identifier.id, temp);652 }653 if (locals.has(value.place.identifier.id)) {654 locals.add(lvalue.identifier.id);655 }656 }657 break;658 }659 case 'DeclareLocal': {660 const local: Temporary = {661 kind: 'Local',662 identifier: value.lvalue.place.identifier,663 path: [],664 context: false,665 loc: value.lvalue.place.loc,666 };667 temporaries.set(value.lvalue.place.identifier.id, local);668 locals.add(value.lvalue.place.identifier.id);669 break;670 }671 case 'StoreLocal': {672 if (value.lvalue.place.identifier.name == null) {673 const temp = temporaries.get(value.value.identifier.id);674 if (temp != null) {675 temporaries.set(value.lvalue.place.identifier.id, temp);676 }677 break;678 }679 visit(value.value);680 if (value.lvalue.kind !== InstructionKind.Reassign) {681 const local: Temporary = {682 kind: 'Local',683 identifier: value.lvalue.place.identifier,684 path: [],685 context: false,686 loc: value.lvalue.place.loc,687 };688 temporaries.set(value.lvalue.place.identifier.id, local);689 locals.add(value.lvalue.place.identifier.id);690 }691 break;692 }693 case 'DeclareContext': {694 const local: Temporary = {695 kind: 'Local',696 identifier: value.lvalue.place.identifier,697 path: [],698 context: true,699 loc: value.lvalue.place.loc,700 };701 temporaries.set(value.lvalue.place.identifier.id, local);702 break;703 }704 case 'StoreContext': {705 visit(value.value);706 if (value.lvalue.kind !== InstructionKind.Reassign) {707 const local: Temporary = {708 kind: 'Local',709 identifier: value.lvalue.place.identifier,710 path: [],711 context: true,712 loc: value.lvalue.place.loc,713 };714 temporaries.set(value.lvalue.place.identifier.id, local);715 locals.add(value.lvalue.place.identifier.id);716 }717 break;718 }719 case 'Destructure': {720 visit(value.value);721 if (value.lvalue.kind !== InstructionKind.Reassign) {722 for (const lvalue of eachInstructionValueLValue(value)) {723 const local: Temporary = {724 kind: 'Local',725 identifier: lvalue.identifier,726 path: [],727 context: false,728 loc: lvalue.loc,729 };730 temporaries.set(lvalue.identifier.id, local);731 locals.add(lvalue.identifier.id);732 }733 }734 break;735 }736 case 'PropertyLoad': {737 if (738 typeof value.property === 'number' ||739 (isUseRefType(value.object.identifier) &&740 value.property === 'current')741 ) {742 visit(value.object);743 break;744 }745 const object = temporaries.get(value.object.identifier.id);746 if (object != null && object.kind === 'Local') {747 const optional = optionals.get(value.object.identifier.id) ?? false;748 const local: Temporary = {749 kind: 'Local',750 identifier: object.identifier,751 context: object.context,752 path: [753 ...object.path,754 {755 optional,756 property: value.property,757 loc: value.loc,758 },759 ],760 loc: value.loc,761 };762 temporaries.set(lvalue.identifier.id, local);763 }764 break;765 }766 case 'FunctionExpression':767 case 'ObjectMethod': {768 const functionDeps = collectDependencies(769 value.loweredFunc.func,770 temporaries,771 null,772 true, // isFunctionExpression773 );774 temporaries.set(lvalue.identifier.id, functionDeps);775 addDependency(functionDeps, dependencies, locals);776 break;777 }778 case 'StartMemoize': {779 const onStartMemoize = callbacks?.onStartMemoize;780 if (onStartMemoize != null) {781 onStartMemoize(value, dependencies, locals);782 }783 break;784 }785 case 'FinishMemoize': {786 const onFinishMemoize = callbacks?.onFinishMemoize;787 if (onFinishMemoize != null) {788 onFinishMemoize(value, dependencies, locals);789 }790 break;791 }792 case 'ArrayExpression': {793 const arrayDeps: Set<InferredDependency> = new Set();794 for (const item of value.elements) {795 if (item.kind === 'Hole') {796 continue;797 }798 const place = item.kind === 'Identifier' ? item : item.place;799 // Visit with alternative deps/locals to record manual dependencies800 visitCandidateDependency(place, temporaries, arrayDeps, new Set());801 // Visit normally to propagate inferred dependencies upward802 visit(place);803 }804 temporaries.set(lvalue.identifier.id, {805 kind: 'Aggregate',806 dependencies: arrayDeps,807 loc: value.loc,808 });809 break;810 }811 case 'CallExpression':812 case 'MethodCall': {813 const receiver =814 value.kind === 'CallExpression' ? value.callee : value.property;815816 const onEffect = callbacks?.onEffect;817 if (onEffect != null && isEffectHook(receiver.identifier)) {818 const [fn, deps] = value.args;819 if (fn?.kind === 'Identifier' && deps?.kind === 'Identifier') {820 const fnDeps = temporaries.get(fn.identifier.id);821 const manualDeps = temporaries.get(deps.identifier.id);822 if (823 fnDeps?.kind === 'Aggregate' &&824 manualDeps?.kind === 'Aggregate'825 ) {826 onEffect(827 fnDeps.dependencies,828 manualDeps.dependencies,829 manualDeps.loc ?? null,830 );831 }832 }833 }834835 // Ignore the method itself836 for (const operand of eachInstructionValueOperand(value)) {837 if (838 value.kind === 'MethodCall' &&839 operand.identifier.id === value.property.identifier.id840 ) {841 continue;842 }843 visit(operand);844 }845 break;846 }847 default: {848 for (const operand of eachInstructionValueOperand(value)) {849 visit(operand);850 }851 for (const lvalue of eachInstructionLValue(instr)) {852 locals.add(lvalue.identifier.id);853 }854 }855 }856 }857 for (const operand of eachTerminalOperand(block.terminal)) {858 if (optionals.has(operand.identifier.id)) {859 continue;860 }861 visit(operand);862 }863 }864 return {kind: 'Aggregate', dependencies};865}866867function printInferredDependency(dep: InferredDependency): string {868 switch (dep.kind) {869 case 'Global': {870 return dep.binding.name;871 }872 case 'Local': {873 CompilerError.invariant(874 dep.identifier.name != null && dep.identifier.name.kind === 'named',875 {876 reason: 'Expected dependencies to be named variables',877 loc: dep.loc,878 },879 );880 return `${dep.identifier.name.value}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;881 }882 }883}884885function printManualMemoDependency(dep: ManualMemoDependency): string {886 let identifierName: string;887 if (dep.root.kind === 'Global') {888 identifierName = dep.root.identifierName;889 } else {890 const name = dep.root.value.identifier.name;891 CompilerError.invariant(name != null && name.kind === 'named', {892 reason: 'Expected manual dependencies to be named variables',893 loc: dep.root.value.loc,894 });895 identifierName = name.value;896 }897 return `${identifierName}${dep.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;898}899900function isEqualTemporary(a: Temporary, b: Temporary): boolean {901 switch (a.kind) {902 case 'Aggregate': {903 return false;904 }905 case 'Global': {906 return b.kind === 'Global' && a.binding.name === b.binding.name;907 }908 case 'Local': {909 return (910 b.kind === 'Local' &&911 a.identifier.id === b.identifier.id &&912 areEqualPaths(a.path, b.path)913 );914 }915 }916}917918type Temporary =919 | {kind: 'Global'; binding: LoadGlobal['binding']}920 | {921 kind: 'Local';922 identifier: Identifier;923 path: DependencyPath;924 context: boolean;925 loc: SourceLocation;926 }927 | {928 kind: 'Aggregate';929 dependencies: Set<InferredDependency>;930 loc?: SourceLocation;931 };932type InferredDependency = Extract<Temporary, {kind: 'Local' | 'Global'}>;933934function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {935 const reactive = new Set<IdentifierId>();936 for (const block of fn.body.blocks.values()) {937 for (const instr of block.instructions) {938 for (const lvalue of eachInstructionLValue(instr)) {939 if (lvalue.reactive) {940 reactive.add(lvalue.identifier.id);941 }942 }943 for (const operand of eachInstructionValueOperand(instr.value)) {944 if (operand.reactive) {945 reactive.add(operand.identifier.id);946 }947 }948 }949 for (const operand of eachTerminalOperand(block.terminal)) {950 if (operand.reactive) {951 reactive.add(operand.identifier.id);952 }953 }954 }955 return reactive;956}957958export function findOptionalPlaces(959 fn: HIRFunction,960): Map<IdentifierId, boolean> {961 const optionals = new Map<IdentifierId, boolean>();962 const visited: Set<BlockId> = new Set();963 for (const [, block] of fn.body.blocks) {964 if (visited.has(block.id)) {965 continue;966 }967 if (block.terminal.kind === 'optional') {968 visited.add(block.id);969 const optionalTerminal = block.terminal;970 let testBlock = fn.body.blocks.get(block.terminal.test)!;971 const queue: Array<boolean | null> = [block.terminal.optional];972 loop: while (true) {973 visited.add(testBlock.id);974 const terminal = testBlock.terminal;975 switch (terminal.kind) {976 case 'branch': {977 const isOptional = queue.pop();978 CompilerError.invariant(isOptional !== undefined, {979 reason:980 'Expected an optional value for each optional test condition',981 loc: terminal.test.loc,982 });983 if (isOptional != null) {984 optionals.set(terminal.test.identifier.id, isOptional);985 }986 if (terminal.fallthrough === optionalTerminal.fallthrough) {987 // found it988 const consequent = fn.body.blocks.get(terminal.consequent)!;989 const last = consequent.instructions.at(-1);990 if (last !== undefined && last.value.kind === 'StoreLocal') {991 if (isOptional != null) {992 optionals.set(last.value.value.identifier.id, isOptional);993 }994 }995 break loop;996 } else {997 testBlock = fn.body.blocks.get(terminal.fallthrough)!;998 }999 break;1000 }1001 case 'optional': {1002 queue.push(terminal.optional);1003 testBlock = fn.body.blocks.get(terminal.test)!;1004 break;1005 }1006 case 'logical':1007 case 'ternary': {1008 queue.push(null);1009 testBlock = fn.body.blocks.get(terminal.test)!;1010 break;1011 }10121013 case 'sequence': {1014 // Do we need sequence?? In any case, don't push to queue bc there is no corresponding branch terminal1015 testBlock = fn.body.blocks.get(terminal.block)!;1016 break;1017 }1018 case 'maybe-throw': {1019 testBlock = fn.body.blocks.get(terminal.continuation)!;1020 break;1021 }1022 default: {1023 CompilerError.invariant(false, {1024 reason: `Unexpected terminal in optional`,1025 message: `Unexpected ${terminal.kind} in optional`,1026 loc: terminal.loc,1027 });1028 }1029 }1030 }1031 CompilerError.invariant(queue.length === 0, {1032 reason:1033 'Expected a matching number of conditional blocks and branch points',1034 loc: block.terminal.loc,1035 });1036 }1037 }1038 return optionals;1039}10401041function isOptionalDependency(1042 inferredDependency: Extract<InferredDependency, {kind: 'Local'}>,1043 reactive: Set<IdentifierId>,1044): boolean {1045 return (1046 !reactive.has(inferredDependency.identifier.id) &&1047 (isStableType(inferredDependency.identifier) ||1048 isPrimitiveType(inferredDependency.identifier))1049 );1050}10511052function createDiagnostic(1053 category:1054 | ErrorCategory.MemoDependencies1055 | ErrorCategory.EffectExhaustiveDependencies,1056 missing: Array<InferredDependency>,1057 extra: Array<ManualMemoDependency>,1058 suggestion: CompilerSuggestion | null,1059): CompilerDiagnostic {1060 let reason: string;1061 let description: string;10621063 function joinMissingExtraDetail(1064 missingString: string,1065 extraString: string,1066 joinStr: string,1067 ): string {1068 return [1069 missing.length !== 0 ? missingString : null,1070 extra.length !== 0 ? extraString : null,1071 ]1072 .filter(Boolean)1073 .join(joinStr);1074 }10751076 switch (category) {1077 case ErrorCategory.MemoDependencies: {1078 reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} memoization dependencies`;1079 description = joinMissingExtraDetail(1080 'Missing dependencies can cause a value to update less often than it should, resulting in stale UI',1081 'Extra dependencies can cause a value to update more often than it should, resulting in performance' +1082 ' problems such as excessive renders or effects firing too often',1083 '. ',1084 );1085 break;1086 }1087 case ErrorCategory.EffectExhaustiveDependencies: {1088 reason = `Found ${joinMissingExtraDetail('missing', 'extra', '/')} effect dependencies`;1089 description = joinMissingExtraDetail(1090 'Missing dependencies can cause an effect to fire less often than it should',1091 'Extra dependencies can cause an effect to fire more often than it should, resulting' +1092 ' in performance problems such as excessive renders and side effects',1093 '. ',1094 );1095 break;1096 }1097 default: {1098 CompilerError.invariant(false, {1099 reason: `Unexpected error category: ${category}`,1100 loc: GeneratedSource,1101 });1102 }1103 }11041105 return CompilerDiagnostic.create({1106 category,1107 reason,1108 description,1109 suggestions: suggestion != null ? [suggestion] : null,1110 });1111}11121113export function isEffectHook(identifier: Identifier): boolean {1114 return (1115 isUseEffectHookType(identifier) ||1116 isUseLayoutEffectHookType(identifier) ||1117 isUseInsertionEffectHookType(identifier)1118 );1119}
Findings
✓ No findings reported for this file.