compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts TYPESCRIPT 844 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 {CompilerError, SourceLocation} from '..';9import {10  BlockId,11  Effect,12  HIRFunction,13  Identifier,14  IdentifierId,15  InstructionId,16  isJsxType,17  makeInstructionId,18  ValueKind,19  ValueReason,20  Place,21  isPrimitiveType,22} from '../HIR/HIR';23import {Environment} from '../HIR/Environment';24import {25  eachInstructionLValue,26  eachInstructionValueOperand,27  eachTerminalOperand,28} from '../HIR/visitors';29import {assertExhaustive, getOrInsertWith} from '../Utils/utils';3031import {AliasingEffect, MutationReason} from './AliasingEffects';3233/**34 * This pass builds an abstract model of the heap and interprets the effects of the35 * given function in order to determine the following:36 * - The mutable ranges of all identifiers in the function37 * - The externally-visible effects of the function, such as mutations of params and38 *   context-vars, aliasing between params/context-vars/return-value, and impure side39 *   effects.40 * - The legacy `Effect` to store on each Place.41 *42 * This pass builds a data flow graph using the effects, tracking an abstract notion43 * of "when" each effect occurs relative to the others. It then walks each mutation44 * effect against the graph, updating the range of each node that would be reachable45 * at the "time" that the effect occurred.46 *47 * This pass also validates against invalid effects: any function that is reachable48 * by being called, or via a Render effect, is validated against mutating globals49 * or calling impure code.50 *51 * Note that this function also populates the outer function's aliasing effects with52 * any mutations that apply to its params or context variables.53 *54 * ## Example55 * A function expression such as the following:56 *57 * ```58 * (x) => { x.y = true }59 * ```60 *61 * Would populate a `Mutate x` aliasing effect on the outer function.62 *63 * ## Returned Function Effects64 *65 * The function returns (if successful) a list of externally-visible effects.66 * This is determined by simulating a conditional, transitive mutation against67 * each param, context variable, and return value in turn, and seeing which other68 * such values are affected. If they're affected, they must be captured, so we69 * record a Capture.70 *71 * The only tricky bit is the return value, which could _alias_ (or even assign)72 * one or more of the params/context-vars rather than just capturing. So we have73 * to do a bit more tracking for returns.74 */75export function inferMutationAliasingRanges(76  fn: HIRFunction,77  {isFunctionExpression}: {isFunctionExpression: boolean},78): Array<AliasingEffect> {79  // The set of externally-visible effects80  const functionEffects: Array<AliasingEffect> = [];8182  /**83   * Part 1: Infer mutable ranges for values. We build an abstract model of84   * values, the alias/capture edges between them, and the set of mutations.85   * Edges and mutations are ordered, with mutations processed against the86   * abstract model only after it is fully constructed by visiting all blocks87   * _and_ connecting phis. Phis are considered ordered at the time of the88   * phi node.89   *90   * This should (may?) mean that mutations are able to see the full state91   * of the graph and mark all the appropriate identifiers as mutated at92   * the correct point, accounting for both backward and forward edges.93   * Ie a mutation of x accounts for both values that flowed into x,94   * and values that x flowed into.95   */96  const state = new AliasingState();97  type PendingPhiOperand = {from: Place; into: Place; index: number};98  const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();99  const mutations: Array<{100    index: number;101    id: InstructionId;102    transitive: boolean;103    kind: MutationKind;104    place: Place;105    reason: MutationReason | null;106  }> = [];107  const renders: Array<{index: number; place: Place}> = [];108109  let index = 0;110111  const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;112113  for (const param of [...fn.params, ...fn.context, fn.returns]) {114    const place = param.kind === 'Identifier' ? param : param.place;115    state.create(place, {kind: 'Object'});116  }117  const seenBlocks = new Set<BlockId>();118  for (const block of fn.body.blocks.values()) {119    for (const phi of block.phis) {120      state.create(phi.place, {kind: 'Phi'});121      for (const [pred, operand] of phi.operands) {122        if (!seenBlocks.has(pred)) {123          // NOTE: annotation required to actually typecheck and not silently infer `any`124          const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(125            pendingPhis,126            pred,127            () => [],128          );129          blockPhis.push({from: operand, into: phi.place, index: index++});130        } else {131          state.assign(index++, operand, phi.place);132        }133      }134    }135    seenBlocks.add(block.id);136137    for (const instr of block.instructions) {138      if (instr.effects == null) continue;139      for (const effect of instr.effects) {140        if (effect.kind === 'Create') {141          state.create(effect.into, {kind: 'Object'});142        } else if (effect.kind === 'CreateFunction') {143          state.create(effect.into, {144            kind: 'Function',145            function: effect.function.loweredFunc.func,146          });147        } else if (effect.kind === 'CreateFrom') {148          state.createFrom(index++, effect.from, effect.into);149        } else if (effect.kind === 'Assign') {150          /**151           * TODO: Invariant that the node is not initialized yet152           *153           * InferFunctionExpressionAliasingEffectSignatures currently infers154           * Assign effects in some places that should be Alias, leading to155           * Assign effects that reinitialize a value. The end result appears to156           * be fine, but we should fix that inference pass so that we add the157           * invariant here.158           */159          if (!state.nodes.has(effect.into.identifier)) {160            state.create(effect.into, {kind: 'Object'});161          }162          state.assign(index++, effect.from, effect.into);163        } else if (effect.kind === 'Alias') {164          state.assign(index++, effect.from, effect.into);165        } else if (effect.kind === 'MaybeAlias') {166          state.maybeAlias(index++, effect.from, effect.into);167        } else if (effect.kind === 'Capture') {168          state.capture(index++, effect.from, effect.into);169        } else if (170          effect.kind === 'MutateTransitive' ||171          effect.kind === 'MutateTransitiveConditionally'172        ) {173          mutations.push({174            index: index++,175            id: instr.id,176            transitive: true,177            kind:178              effect.kind === 'MutateTransitive'179                ? MutationKind.Definite180                : MutationKind.Conditional,181            reason: null,182            place: effect.value,183          });184        } else if (185          effect.kind === 'Mutate' ||186          effect.kind === 'MutateConditionally'187        ) {188          mutations.push({189            index: index++,190            id: instr.id,191            transitive: false,192            kind:193              effect.kind === 'Mutate'194                ? MutationKind.Definite195                : MutationKind.Conditional,196            reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,197            place: effect.value,198          });199        } else if (200          effect.kind === 'MutateFrozen' ||201          effect.kind === 'MutateGlobal' ||202          effect.kind === 'Impure'203        ) {204          if (shouldRecordErrors) {205            fn.env.recordError(effect.error);206          }207          functionEffects.push(effect);208        } else if (effect.kind === 'Render') {209          renders.push({index: index++, place: effect.place});210          functionEffects.push(effect);211        }212      }213    }214    const blockPhis = pendingPhis.get(block.id);215    if (blockPhis != null) {216      for (const {from, into, index} of blockPhis) {217        state.assign(index, from, into);218      }219    }220    if (block.terminal.kind === 'return') {221      state.assign(index++, block.terminal.value, fn.returns);222    }223224    if (225      (block.terminal.kind === 'maybe-throw' ||226        block.terminal.kind === 'return') &&227      block.terminal.effects != null228    ) {229      for (const effect of block.terminal.effects) {230        if (effect.kind === 'Alias') {231          state.assign(index++, effect.from, effect.into);232        } else {233          CompilerError.invariant(effect.kind === 'Freeze', {234            reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,235            loc: block.terminal.loc,236          });237        }238      }239    }240  }241242  for (const mutation of mutations) {243    state.mutate(244      mutation.index,245      mutation.place.identifier,246      makeInstructionId(mutation.id + 1),247      mutation.transitive,248      mutation.kind,249      mutation.place.loc,250      mutation.reason,251      shouldRecordErrors ? fn.env : null,252    );253  }254  for (const render of renders) {255    state.render(256      render.index,257      render.place.identifier,258      shouldRecordErrors ? fn.env : null,259    );260  }261  for (const param of [...fn.context, ...fn.params]) {262    const place = param.kind === 'Identifier' ? param : param.place;263264    const node = state.nodes.get(place.identifier);265    if (node == null) {266      continue;267    }268    let mutated = false;269    if (node.local != null) {270      if (node.local.kind === MutationKind.Conditional) {271        mutated = true;272        functionEffects.push({273          kind: 'MutateConditionally',274          value: {...place, loc: node.local.loc},275        });276      } else if (node.local.kind === MutationKind.Definite) {277        mutated = true;278        functionEffects.push({279          kind: 'Mutate',280          value: {...place, loc: node.local.loc},281          reason: node.mutationReason,282        });283      }284    }285    if (node.transitive != null) {286      if (node.transitive.kind === MutationKind.Conditional) {287        mutated = true;288        functionEffects.push({289          kind: 'MutateTransitiveConditionally',290          value: {...place, loc: node.transitive.loc},291        });292      } else if (node.transitive.kind === MutationKind.Definite) {293        mutated = true;294        functionEffects.push({295          kind: 'MutateTransitive',296          value: {...place, loc: node.transitive.loc},297        });298      }299    }300    if (mutated) {301      place.effect = Effect.Capture;302    }303  }304305  /**306   * Part 2307   * Add legacy operand-specific effects based on instruction effects and mutable ranges.308   * Also fixes up operand mutable ranges, making sure that start is non-zero if the value309   * is mutated (depended on by later passes like InferReactiveScopeVariables which uses this310   * to filter spurious mutations of globals, which we now guard against more precisely)311   */312  for (const block of fn.body.blocks.values()) {313    for (const phi of block.phis) {314      // TODO: we don't actually set these effects today!315      phi.place.effect = Effect.Store;316      const isPhiMutatedAfterCreation: boolean =317        phi.place.identifier.mutableRange.end >318        (block.instructions.at(0)?.id ?? block.terminal.id);319      for (const operand of phi.operands.values()) {320        operand.effect = isPhiMutatedAfterCreation321          ? Effect.Capture322          : Effect.Read;323      }324      if (325        isPhiMutatedAfterCreation &&326        phi.place.identifier.mutableRange.start === 0327      ) {328        /*329         * TODO: ideally we'd construct a precise start range, but what really330         * matters is that the phi's range appears mutable (end > start + 1)331         * so we just set the start to the previous instruction before this block332         */333        const firstInstructionIdOfBlock =334          block.instructions.at(0)?.id ?? block.terminal.id;335        phi.place.identifier.mutableRange.start = makeInstructionId(336          firstInstructionIdOfBlock - 1,337        );338      }339    }340    for (const instr of block.instructions) {341      for (const lvalue of eachInstructionLValue(instr)) {342        lvalue.effect = Effect.ConditionallyMutate;343        if (lvalue.identifier.mutableRange.start === 0) {344          lvalue.identifier.mutableRange.start = instr.id;345        }346        if (lvalue.identifier.mutableRange.end === 0) {347          lvalue.identifier.mutableRange.end = makeInstructionId(348            Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),349          );350        }351      }352      for (const operand of eachInstructionValueOperand(instr.value)) {353        operand.effect = Effect.Read;354      }355      if (instr.effects == null) {356        continue;357      }358      const operandEffects = new Map<IdentifierId, Effect>();359      for (const effect of instr.effects) {360        switch (effect.kind) {361          case 'Assign':362          case 'Alias':363          case 'Capture':364          case 'CreateFrom':365          case 'MaybeAlias': {366            const isMutatedOrReassigned =367              effect.into.identifier.mutableRange.end > instr.id;368            if (isMutatedOrReassigned) {369              operandEffects.set(effect.from.identifier.id, Effect.Capture);370              operandEffects.set(effect.into.identifier.id, Effect.Store);371            } else {372              operandEffects.set(effect.from.identifier.id, Effect.Read);373              operandEffects.set(effect.into.identifier.id, Effect.Store);374            }375            break;376          }377          case 'CreateFunction':378          case 'Create': {379            break;380          }381          case 'Mutate': {382            operandEffects.set(effect.value.identifier.id, Effect.Store);383            break;384          }385          case 'Apply': {386            CompilerError.invariant(false, {387              reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,388              loc: effect.function.loc,389            });390          }391          case 'MutateTransitive':392          case 'MutateConditionally':393          case 'MutateTransitiveConditionally': {394            operandEffects.set(395              effect.value.identifier.id,396              Effect.ConditionallyMutate,397            );398            break;399          }400          case 'Freeze': {401            operandEffects.set(effect.value.identifier.id, Effect.Freeze);402            break;403          }404          case 'ImmutableCapture': {405            // no-op, Read is the default406            break;407          }408          case 'Impure':409          case 'Render':410          case 'MutateFrozen':411          case 'MutateGlobal': {412            // no-op413            break;414          }415          default: {416            assertExhaustive(417              effect,418              `Unexpected effect kind ${(effect as any).kind}`,419            );420          }421        }422      }423      for (const lvalue of eachInstructionLValue(instr)) {424        const effect =425          operandEffects.get(lvalue.identifier.id) ??426          Effect.ConditionallyMutate;427        lvalue.effect = effect;428      }429      for (const operand of eachInstructionValueOperand(instr.value)) {430        if (431          operand.identifier.mutableRange.end > instr.id &&432          operand.identifier.mutableRange.start === 0433        ) {434          operand.identifier.mutableRange.start = instr.id;435        }436        const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;437        operand.effect = effect;438      }439440      /**441       * This case is targeted at hoisted functions like:442       *443       * ```444       * x();445       * function x() { ... }446       * ```447       *448       * Which turns into:449       *450       * t0 = DeclareContext HoistedFunction x451       * t1 = LoadContext x452       * t2 = CallExpression t1 ( )453       * t3 = FunctionExpression ...454       * t4 = StoreContext Function x = t3455       *456       * If the function had captured mutable values, it would already have its457       * range extended to include the StoreContext. But if the function doesn't458       * capture any mutable values its range won't have been extended yet. We459       * want to ensure that the value is memoized along with the context variable,460       * not independently of it (bc of the way we do codegen for hoisted functions).461       * So here we check for StoreContext rvalues and if they haven't already had462       * their range extended to at least this instruction, we extend it.463       */464      if (465        instr.value.kind === 'StoreContext' &&466        instr.value.value.identifier.mutableRange.end <= instr.id467      ) {468        instr.value.value.identifier.mutableRange.end = makeInstructionId(469          instr.id + 1,470        );471      }472    }473    if (block.terminal.kind === 'return') {474      block.terminal.value.effect = isFunctionExpression475        ? Effect.Read476        : Effect.Freeze;477    } else {478      for (const operand of eachTerminalOperand(block.terminal)) {479        operand.effect = Effect.Read;480      }481    }482  }483484  /**485   * Part 3486   * Finish populating the externally visible effects. Above we bubble-up the side effects487   * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.488   * Here we populate an effect to create the return value as well as populating alias/capture489   * effects for how data flows between the params, context vars, and return.490   */491  const returns = fn.returns.identifier;492  functionEffects.push({493    kind: 'Create',494    into: fn.returns,495    value: isPrimitiveType(returns)496      ? ValueKind.Primitive497      : isJsxType(returns.type)498        ? ValueKind.Frozen499        : ValueKind.Mutable,500    reason: ValueReason.KnownReturnSignature,501  });502  /**503   * Determine precise data-flow effects by simulating transitive mutations of the params/504   * captures and seeing what other params/context variables are affected. Anything that505   * would be transitively mutated needs a capture relationship.506   */507  const tracked: Array<Place> = [];508  for (const param of [...fn.params, ...fn.context, fn.returns]) {509    const place = param.kind === 'Identifier' ? param : param.place;510    tracked.push(place);511  }512  for (const into of tracked) {513    const mutationIndex = index++;514    state.mutate(515      mutationIndex,516      into.identifier,517      null,518      true,519      MutationKind.Conditional,520      into.loc,521      null,522      null,523    );524    for (const from of tracked) {525      if (526        from.identifier.id === into.identifier.id ||527        from.identifier.id === fn.returns.identifier.id528      ) {529        continue;530      }531      const fromNode = state.nodes.get(from.identifier);532      CompilerError.invariant(fromNode != null, {533        reason: `Expected a node to exist for all parameters and context variables`,534        loc: into.loc,535      });536      if (fromNode.lastMutated === mutationIndex) {537        if (into.identifier.id === fn.returns.identifier.id) {538          // The return value could be any of the params/context variables539          functionEffects.push({540            kind: 'Alias',541            from,542            into,543          });544        } else {545          // Otherwise params/context-vars can only capture each other546          functionEffects.push({547            kind: 'Capture',548            from,549            into,550          });551        }552      }553    }554  }555556  return functionEffects;557}558559function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {560  if (env == null) return;561  for (const effect of fn.aliasingEffects ?? []) {562    switch (effect.kind) {563      case 'Impure':564      case 'MutateFrozen':565      case 'MutateGlobal': {566        env.recordError(effect.error);567        break;568      }569    }570  }571}572573export enum MutationKind {574  None = 0,575  Conditional = 1,576  Definite = 2,577}578579type Node = {580  id: Identifier;581  createdFrom: Map<Identifier, number>;582  captures: Map<Identifier, number>;583  aliases: Map<Identifier, number>;584  maybeAliases: Map<Identifier, number>;585  edges: Array<{586    index: number;587    node: Identifier;588    kind: 'capture' | 'alias' | 'maybeAlias';589  }>;590  transitive: {kind: MutationKind; loc: SourceLocation} | null;591  local: {kind: MutationKind; loc: SourceLocation} | null;592  lastMutated: number;593  mutationReason: MutationReason | null;594  value:595    | {kind: 'Object'}596    | {kind: 'Phi'}597    | {kind: 'Function'; function: HIRFunction};598};599class AliasingState {600  nodes: Map<Identifier, Node> = new Map();601602  create(place: Place, value: Node['value']): void {603    this.nodes.set(place.identifier, {604      id: place.identifier,605      createdFrom: new Map(),606      captures: new Map(),607      aliases: new Map(),608      maybeAliases: new Map(),609      edges: [],610      transitive: null,611      local: null,612      lastMutated: 0,613      mutationReason: null,614      value,615    });616  }617618  createFrom(index: number, from: Place, into: Place): void {619    this.create(into, {kind: 'Object'});620    const fromNode = this.nodes.get(from.identifier);621    const toNode = this.nodes.get(into.identifier);622    if (fromNode == null || toNode == null) {623      return;624    }625    fromNode.edges.push({index, node: into.identifier, kind: 'alias'});626    if (!toNode.createdFrom.has(from.identifier)) {627      toNode.createdFrom.set(from.identifier, index);628    }629  }630631  capture(index: number, from: Place, into: Place): void {632    const fromNode = this.nodes.get(from.identifier);633    const toNode = this.nodes.get(into.identifier);634    if (fromNode == null || toNode == null) {635      return;636    }637    fromNode.edges.push({index, node: into.identifier, kind: 'capture'});638    if (!toNode.captures.has(from.identifier)) {639      toNode.captures.set(from.identifier, index);640    }641  }642643  assign(index: number, from: Place, into: Place): void {644    const fromNode = this.nodes.get(from.identifier);645    const toNode = this.nodes.get(into.identifier);646    if (fromNode == null || toNode == null) {647      return;648    }649    fromNode.edges.push({index, node: into.identifier, kind: 'alias'});650    if (!toNode.aliases.has(from.identifier)) {651      toNode.aliases.set(from.identifier, index);652    }653  }654655  maybeAlias(index: number, from: Place, into: Place): void {656    const fromNode = this.nodes.get(from.identifier);657    const toNode = this.nodes.get(into.identifier);658    if (fromNode == null || toNode == null) {659      return;660    }661    fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});662    if (!toNode.maybeAliases.has(from.identifier)) {663      toNode.maybeAliases.set(from.identifier, index);664    }665  }666667  render(index: number, start: Identifier, env: Environment | null): void {668    const seen = new Set<Identifier>();669    const queue: Array<Identifier> = [start];670    while (queue.length !== 0) {671      const current = queue.pop()!;672      if (seen.has(current)) {673        continue;674      }675      seen.add(current);676      const node = this.nodes.get(current);677      if (node == null || node.transitive != null || node.local != null) {678        continue;679      }680      if (node.value.kind === 'Function') {681        appendFunctionErrors(env, node.value.function);682      }683      for (const [alias, when] of node.createdFrom) {684        if (when >= index) {685          continue;686        }687        queue.push(alias);688      }689      for (const [alias, when] of node.aliases) {690        if (when >= index) {691          continue;692        }693        queue.push(alias);694      }695      for (const [capture, when] of node.captures) {696        if (when >= index) {697          continue;698        }699        queue.push(capture);700      }701    }702  }703704  mutate(705    index: number,706    start: Identifier,707    // Null is used for simulated mutations708    end: InstructionId | null,709    transitive: boolean,710    startKind: MutationKind,711    loc: SourceLocation,712    reason: MutationReason | null,713    env: Environment | null,714  ): void {715    const seen = new Map<Identifier, MutationKind>();716    const queue: Array<{717      place: Identifier;718      transitive: boolean;719      direction: 'backwards' | 'forwards';720      kind: MutationKind;721    }> = [{place: start, transitive, direction: 'backwards', kind: startKind}];722    while (queue.length !== 0) {723      const {place: current, transitive, direction, kind} = queue.pop()!;724      const previousKind = seen.get(current);725      if (previousKind != null && previousKind >= kind) {726        continue;727      }728      seen.set(current, kind);729      const node = this.nodes.get(current);730      if (node == null) {731        continue;732      }733      node.mutationReason ??= reason;734      node.lastMutated = Math.max(node.lastMutated, index);735      if (end != null) {736        node.id.mutableRange.end = makeInstructionId(737          Math.max(node.id.mutableRange.end, end),738        );739      }740      if (741        node.value.kind === 'Function' &&742        node.transitive == null &&743        node.local == null744      ) {745        appendFunctionErrors(env, node.value.function);746      }747      if (transitive) {748        if (node.transitive == null || node.transitive.kind < kind) {749          node.transitive = {kind, loc};750        }751      } else {752        if (node.local == null || node.local.kind < kind) {753          node.local = {kind, loc};754        }755      }756      /**757       * all mutations affect "forward" edges by the rules:758       * - Capture a -> b, mutate(a) => mutate(b)759       * - Alias a -> b, mutate(a) => mutate(b)760       */761      for (const edge of node.edges) {762        if (edge.index >= index) {763          break;764        }765        queue.push({766          place: edge.node,767          transitive,768          direction: 'forwards',769          // Traversing a maybeAlias edge always downgrades to conditional mutation770          kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,771        });772      }773      for (const [alias, when] of node.createdFrom) {774        if (when >= index) {775          continue;776        }777        queue.push({778          place: alias,779          transitive: true,780          direction: 'backwards',781          kind,782        });783      }784      if (direction === 'backwards' || node.value.kind !== 'Phi') {785        /**786         * all mutations affect backward alias edges by the rules:787         * - Alias a -> b, mutate(b) => mutate(a)788         * - Alias a -> b, mutateTransitive(b) => mutate(a)789         *790         * However, if we reached a phi because one of its inputs was mutated791         * (and we're advancing "forwards" through that node's edges), then792         * we know we've already processed the mutation at its source. The793         * phi's other inputs can't be affected.794         */795        for (const [alias, when] of node.aliases) {796          if (when >= index) {797            continue;798          }799          queue.push({800            place: alias,801            transitive,802            direction: 'backwards',803            kind,804          });805        }806        /**807         * MaybeAlias indicates potential data flow from unknown function calls,808         * so we downgrade mutations through these aliases to consider them809         * conditional. This means we'll consider them for mutation *range*810         * purposes but not report validation errors for mutations, since811         * we aren't sure that the `from` value could actually be aliased.812         */813        for (const [alias, when] of node.maybeAliases) {814          if (when >= index) {815            continue;816          }817          queue.push({818            place: alias,819            transitive,820            direction: 'backwards',821            kind: MutationKind.Conditional,822          });823        }824      }825      /**826       * but only transitive mutations affect captures827       */828      if (transitive) {829        for (const [capture, when] of node.captures) {830          if (when >= index) {831            continue;832          }833          queue.push({834            place: capture,835            transitive,836            direction: 'backwards',837            kind,838          });839        }840      }841    }842  }843}

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.