compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts TYPESCRIPT 1,120 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.