packages/react-devtools-shared/src/devtools/store.js JAVASCRIPT 2,586 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,586.
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 *7 * @flow8 */910import {copy} from 'clipboard-js';11import EventEmitter from '../events';12import {inspect} from 'util';13import {14  PROFILING_FLAG_BASIC_SUPPORT,15  PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,16  TREE_OPERATION_ADD,17  TREE_OPERATION_REMOVE,18  TREE_OPERATION_REORDER_CHILDREN,19  TREE_OPERATION_SET_SUBTREE_MODE,20  TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,21  TREE_OPERATION_UPDATE_TREE_BASE_DURATION,22  TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,23  SUSPENSE_TREE_OPERATION_ADD,24  SUSPENSE_TREE_OPERATION_REMOVE,25  SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,26  SUSPENSE_TREE_OPERATION_RESIZE,27  SUSPENSE_TREE_OPERATION_SUSPENDERS,28} from '../constants';29import {30  ElementTypeClass,31  ElementTypeContext,32  ElementTypeFunction,33  ElementTypeForwardRef,34  ElementTypeHostComponent,35  ElementTypeMemo,36  ElementTypeOtherOrUnknown,37  ElementTypeProfiler,38  ElementTypeRoot,39  ElementTypeSuspense,40  ElementTypeSuspenseList,41  ElementTypeTracingMarker,42  ElementTypeVirtual,43  ElementTypeViewTransition,44  ElementTypeActivity,45  ComponentFilterActivitySlice,46} from '../frontend/types';47import {48  getSavedComponentFilters,49  setSavedComponentFilters,50  shallowDiffers,51  utfDecodeStringWithRanges,52  parseElementDisplayNameFromBackend,53  unionOfTwoArrays,54} from '../utils';55import {localStorageGetItem, localStorageSetItem} from '../storage';56import {__DEBUG__} from '../constants';57import {printStore} from './utils';58import ProfilerStore from './ProfilerStore';59import {60  BRIDGE_PROTOCOL,61  currentBridgeProtocol,62} from 'react-devtools-shared/src/bridge';63import {64  StrictMode,65  ActivityHiddenMode,66  ActivityVisibleMode,67} from 'react-devtools-shared/src/frontend/types';68import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';6970import type {71  Element,72  ComponentFilter,73  ElementType,74  SuspenseNode,75  SuspenseTimelineStep,76  Rect,77} from 'react-devtools-shared/src/frontend/types';78import type {79  FrontendBridge,80  BridgeProtocol,81} from 'react-devtools-shared/src/bridge';82import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';83import type {DevToolsHookSettings} from '../backend/types';8485import RBush from 'rbush';8687// Custom version which works with our Rect data structure.88class RectRBush extends RBush<Rect> {89  toBBox(rect: Rect): {90    minX: number,91    minY: number,92    maxX: number,93    maxY: number,94  } {95    return {96      minX: rect.x,97      minY: rect.y,98      maxX: rect.x + rect.width,99      maxY: rect.y + rect.height,100    };101  }102  compareMinX(a: Rect, b: Rect): number {103    return a.x - b.x;104  }105  compareMinY(a: Rect, b: Rect): number {106    return a.y - b.y;107  }108}109110const debug = (methodName: string, ...args: Array<string>) => {111  // $FlowFixMe[constant-condition]112  if (__DEBUG__) {113    console.log(114      `%cStore %c${methodName}`,115      'color: green; font-weight: bold;',116      'font-weight: bold;',117      ...args,118    );119  }120};121122const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =123  'React::DevTools::collapseNodesByDefault';124const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =125  'React::DevTools::recordChangeDescriptions';126127type ErrorAndWarningTuples = Array<{id: number, index: number}>;128129export type Config = {130  checkBridgeProtocolCompatibility?: boolean,131  isProfiling?: boolean,132  supportsInspectMatchingDOMElement?: boolean,133  supportsClickToInspect?: boolean,134  supportsReloadAndProfile?: boolean,135  supportsTraceUpdates?: boolean,136};137138const ADVANCED_PROFILING_NONE = 0;139const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2;140type AdvancedProfiling = 0 | 2;141142export type Capabilities = {143  supportsBasicProfiling: boolean,144  hasOwnerMetadata: boolean,145  supportsStrictMode: boolean,146  supportsAdvancedProfiling: AdvancedProfiling,147};148149function isNonZeroRect(rect: Rect) {150  return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;151}152153function parseElementType(value: number): ElementType | null {154  // Cast before switching so Flow checks exhaustiveness while the default rejects unknown bridge values.155  const type = value as any as ElementType;156  switch (type) {157    case ElementTypeClass:158    case ElementTypeContext:159    case ElementTypeFunction:160    case ElementTypeForwardRef:161    case ElementTypeHostComponent:162    case ElementTypeMemo:163    case ElementTypeOtherOrUnknown:164    case ElementTypeProfiler:165    case ElementTypeRoot:166    case ElementTypeSuspense:167    case ElementTypeSuspenseList:168    case ElementTypeTracingMarker:169    case ElementTypeVirtual:170    case ElementTypeViewTransition:171    case ElementTypeActivity:172      return type;173    default:174      (type) as empty;175      return null;176  }177}178179/**180 * The store is the single source of truth for updates from the backend.181 * ContextProviders can subscribe to the Store for specific things they want to provide.182 */183export default class Store extends EventEmitter<{184  backendVersion: [],185  collapseNodesByDefault: [],186  componentFilters: [],187  error: [Error],188  hookSettings: [$ReadOnly<DevToolsHookSettings>],189  hostInstanceSelected: [Element['id'] | null],190  settingsUpdated: [$ReadOnly<DevToolsHookSettings>, Array<ComponentFilter>],191  mutated: [192    [193      Array<Element['id']>,194      Map<Element['id'], Element['id']>,195      Element['id'] | null,196    ],197  ],198  recordChangeDescriptions: [],199  roots: [],200  rootSupportsBasicProfiling: [],201  rootSupportsPerformanceTracks: [],202  suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],203  supportsNativeStyleEditor: [],204  supportsReloadAndProfile: [],205  unsupportedBridgeProtocolDetected: [],206  unsupportedRendererVersionDetected: [],207}> {208  // If the backend version is new enough to report its (NPM) version, this is it.209  // This version may be displayed by the frontend for debugging purposes.210  _backendVersion: string | null = null;211212  _bridge: FrontendBridge;213214  // Computed whenever _errorsAndWarnings Map changes.215  _cachedComponentWithErrorCount: number = 0;216  _cachedComponentWithWarningCount: number = 0;217  _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;218219  // Should new nodes be collapsed by default when added to the tree?220  _collapseNodesByDefault: boolean = true;221222  _componentFilters: Array<ComponentFilter>;223224  // Map of ID to number of recorded error and warning message IDs.225  _errorsAndWarnings: Map<226    Element['id'],227    {errorCount: number, warningCount: number},228  > = new Map();229230  _focusedTransition: 0 | Element['id'] = 0;231232  // At least one of the injected renderers contains (DEV only) owner metadata.233  _hasOwnerMetadata: boolean = false;234235  // Map of ID to (mutable) Element.236  // Elements are mutated to avoid excessive cloning during tree updates.237  // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.238  _idToElement: Map<Element['id'], Element> = new Map();239240  _idToSuspense: Map<SuspenseNode['id'], SuspenseNode> = new Map();241242  // Should the React Native style editor panel be shown?243  _isNativeStyleEditorSupported: boolean = false;244245  _nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;246247  // Older backends don't support an explicit bridge protocol,248  // so we should timeout eventually and show a downgrade message.249  _onBridgeProtocolTimeoutID: TimeoutID | null = null;250251  // Map of element (id) to the set of elements (ids) it owns.252  // This map enables getOwnersListForElement() to avoid traversing the entire tree.253  _ownersMap: Map<Element['id'], Set<Element['id']>> = new Map();254255  _profilerStore: ProfilerStore;256257  _recordChangeDescriptions: boolean = false;258259  // Incremented each time the store is mutated.260  // This enables a passive effect to detect a mutation between render and commit phase.261  _revision: number = 0;262  _revisionSuspense: number = 0;263264  // This Array must be treated as immutable!265  // Passive effects will check it for changes between render and mount.266  _roots: $ReadOnlyArray<Element['id']> = [];267268  _rootIDToCapabilities: Map<Element['id'], Capabilities> = new Map();269270  // Renderer ID is needed to support inspection fiber props, state, and hooks.271  _rootIDToRendererID: Map<Element['id'], number> = new Map();272273  // Stores all the SuspenseNode rects in an R-tree to make it fast to find overlaps.274  _rtree: RBush<Rect> = new RectRBush();275276  // These options may be initially set by a configuration option when constructing the Store.277  _supportsInspectMatchingDOMElement: boolean = false;278  _supportsClickToInspect: boolean = false;279  _supportsTraceUpdates: boolean = false;280281  _isReloadAndProfileFrontendSupported: boolean = false;282  _isReloadAndProfileBackendSupported: boolean = false;283284  // These options default to false but may be updated as roots are added and removed.285  _rootSupportsBasicProfiling: boolean = false;286  _rootSupportsPerformanceTracks: boolean = false;287288  _bridgeProtocol: BridgeProtocol | null = null;289  _unsupportedBridgeProtocolDetected: boolean = false;290  _unsupportedRendererVersionDetected: boolean = false;291292  // Total number of visible elements (within all roots).293  // Used for windowing purposes.294  _weightAcrossRoots: number = 0;295296  _shouldCheckBridgeProtocolCompatibility: boolean = false;297  _hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;298  _shouldShowWarningsAndErrors: boolean = false;299300  // Only used in browser extension for synchronization with built-in Elements panel.301  _lastSelectedHostInstanceElementId: Element['id'] | null = null;302303  // Maximum recorded node depth during the lifetime of this Store.304  // Can only increase: not guaranteed to return maximal value for currently recorded elements.305  _maximumRecordedDepth = 0;306307  constructor(bridge: FrontendBridge, config?: Config) {308    super();309310    // $FlowFixMe[constant-condition]311    if (__DEBUG__) {312      debug('constructor', 'subscribing to Bridge');313    }314315    this._collapseNodesByDefault =316      localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) ===317      'true';318319    this._recordChangeDescriptions =320      localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===321      'true';322323    this._componentFilters = getSavedComponentFilters();324325    let isProfiling = false;326    if (config != null) {327      isProfiling = config.isProfiling === true;328329      const {330        supportsInspectMatchingDOMElement,331        supportsClickToInspect,332        supportsReloadAndProfile,333        supportsTraceUpdates,334        checkBridgeProtocolCompatibility,335      } = config;336      if (supportsInspectMatchingDOMElement) {337        this._supportsInspectMatchingDOMElement = true;338      }339      if (supportsClickToInspect) {340        this._supportsClickToInspect = true;341      }342      if (supportsReloadAndProfile) {343        this._isReloadAndProfileFrontendSupported = true;344      }345      if (supportsTraceUpdates) {346        this._supportsTraceUpdates = true;347      }348      if (checkBridgeProtocolCompatibility) {349        this._shouldCheckBridgeProtocolCompatibility = true;350      }351    }352353    this._bridge = bridge;354    bridge.addListener('operations', this.onBridgeOperations);355    bridge.addListener('shutdown', this.onBridgeShutdown);356    bridge.addListener(357      'isReloadAndProfileSupportedByBackend',358      this.onBackendReloadAndProfileSupported,359    );360    bridge.addListener(361      'isNativeStyleEditorSupported',362      this.onBridgeNativeStyleEditorSupported,363    );364    bridge.addListener(365      'unsupportedRendererVersion',366      this.onBridgeUnsupportedRendererVersion,367    );368369    this._profilerStore = new ProfilerStore(bridge, this, isProfiling);370371    bridge.addListener('backendVersion', this.onBridgeBackendVersion);372    bridge.addListener('saveToClipboard', this.onSaveToClipboard);373    bridge.addListener('hookSettings', this.onHookSettings);374    bridge.addListener('backendInitialized', this.onBackendInitialized);375    bridge.addListener('selectElement', this.onHostInstanceSelected);376  }377378  // This is only used in tests to avoid memory leaks.379  assertExpectedRootMapSizes() {380    if (this.roots.length === 0) {381      // The only safe time to assert these maps are empty is when the store is empty.382      this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');383      this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');384    }385386    // These maps should always be the same size as the number of roots387    this.assertMapSizeMatchesRootCount(388      this._rootIDToCapabilities,389      '_rootIDToCapabilities',390    );391    this.assertMapSizeMatchesRootCount(392      this._rootIDToRendererID,393      '_rootIDToRendererID',394    );395  }396397  // This is only used in tests to avoid memory leaks.398  assertMapSizeMatchesRootCount<K, V>(map: Map<K, V>, mapName: string) {399    const expectedSize = this.roots.length;400    if (map.size !== expectedSize) {401      this._throwAndEmitError(402        Error(403          `Expected ${mapName} to contain ${expectedSize} items, but it contains ${404            map.size405          } items\n\n${inspect(map, {406            depth: 20,407          })}`,408        ),409      );410    }411  }412413  get backendVersion(): string | null {414    return this._backendVersion;415  }416417  get collapseNodesByDefault(): boolean {418    return this._collapseNodesByDefault;419  }420  set collapseNodesByDefault(value: boolean): void {421    this._collapseNodesByDefault = value;422423    localStorageSetItem(424      LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY,425      value ? 'true' : 'false',426    );427428    this.emit('collapseNodesByDefault');429  }430431  get componentFilters(): Array<ComponentFilter> {432    return this._componentFilters;433  }434  set componentFilters(value: Array<ComponentFilter>): void {435    if (this._profilerStore.isProfilingBasedOnUserInput) {436      // Re-mounting a tree while profiling is in progress might break a lot of assumptions.437      // If necessary, we could support this- but it doesn't seem like a necessary use case.438      this._throwAndEmitError(439        Error('Cannot modify filter preferences while profiling'),440      );441    }442443    // Filter updates are expensive to apply (since they impact the entire tree).444    // Let's determine if they've changed and avoid doing this work if they haven't.445    const prevEnabledComponentFilters = this._componentFilters.filter(446      filter => filter.isEnabled,447    );448    const nextEnabledComponentFilters = value.filter(449      filter => filter.isEnabled,450    );451    let haveEnabledFiltersChanged =452      prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;453    if (!haveEnabledFiltersChanged) {454      for (let i = 0; i < nextEnabledComponentFilters.length; i++) {455        const prevFilter = prevEnabledComponentFilters[i];456        const nextFilter = nextEnabledComponentFilters[i];457        if (shallowDiffers(prevFilter, nextFilter)) {458          haveEnabledFiltersChanged = true;459          break;460        }461      }462    }463464    this._componentFilters = value;465466    // Update persisted filter preferences467    setSavedComponentFilters(value);468    if (this._hookSettings === null) {469      // We changed filters before we got the hook settings.470      // Wait for hook settings before persisting component filters to not overwrite471      // persisted hook settings with defaults.472      // This exists purely as a type safety check; in practice the hook settings473      // should have arrived before any filter changes could be made.474      const onHookSettings = (settings: $ReadOnly<DevToolsHookSettings>) => {475        this._bridge.removeListener('hookSettings', onHookSettings);476        this.emit('settingsUpdated', settings, value);477      };478      this._bridge.addListener('hookSettings', onHookSettings);479      this._bridge.send('getHookSettings');480    } else {481      this.emit('settingsUpdated', this._hookSettings, value);482    }483484    // Notify the renderer that filter preferences have changed.485    // This is an expensive operation; it unmounts and remounts the entire tree,486    // so only do it if the set of enabled component filters has changed.487    if (haveEnabledFiltersChanged) {488      this._bridge.send('updateComponentFilters', value);489    }490491    this.emit('componentFilters');492  }493494  get bridgeProtocol(): BridgeProtocol | null {495    return this._bridgeProtocol;496  }497498  get componentWithErrorCount(): number {499    if (!this._shouldShowWarningsAndErrors) {500      return 0;501    }502503    return this._cachedComponentWithErrorCount;504  }505506  get componentWithWarningCount(): number {507    if (!this._shouldShowWarningsAndErrors) {508      return 0;509    }510511    return this._cachedComponentWithWarningCount;512  }513514  get displayingErrorsAndWarningsEnabled(): boolean {515    return this._shouldShowWarningsAndErrors;516  }517518  get hasOwnerMetadata(): boolean {519    return this._hasOwnerMetadata;520  }521522  get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {523    return this._nativeStyleEditorValidAttributes;524  }525526  get numElements(): number {527    return this._weightAcrossRoots;528  }529530  get profilerStore(): ProfilerStore {531    return this._profilerStore;532  }533534  get recordChangeDescriptions(): boolean {535    return this._recordChangeDescriptions;536  }537  set recordChangeDescriptions(value: boolean): void {538    this._recordChangeDescriptions = value;539540    localStorageSetItem(541      LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,542      value ? 'true' : 'false',543    );544545    this.emit('recordChangeDescriptions');546  }547548  get revision(): number {549    return this._revision;550  }551  get revisionSuspense(): number {552    return this._revisionSuspense;553  }554555  get rootIDToRendererID(): Map<number, number> {556    return this._rootIDToRendererID;557  }558559  get roots(): $ReadOnlyArray<number> {560    return this._roots;561  }562563  // At least one of the currently mounted roots support the Legacy profiler.564  get rootSupportsBasicProfiling(): boolean {565    return this._rootSupportsBasicProfiling;566  }567568  // At least one of the currently mounted roots support performance tracks.569  get rootSupportsPerformanceTracks(): boolean {570    return this._rootSupportsPerformanceTracks;571  }572573  get supportsInspectMatchingDOMElement(): boolean {574    return this._supportsInspectMatchingDOMElement;575  }576577  get supportsClickToInspect(): boolean {578    return this._supportsClickToInspect;579  }580581  get supportsNativeStyleEditor(): boolean {582    return this._isNativeStyleEditorSupported;583  }584585  get supportsReloadAndProfile(): boolean {586    return (587      this._isReloadAndProfileFrontendSupported &&588      this._isReloadAndProfileBackendSupported589    );590  }591592  get supportsTraceUpdates(): boolean {593    return this._supportsTraceUpdates;594  }595596  get unsupportedBridgeProtocolDetected(): boolean {597    return this._unsupportedBridgeProtocolDetected;598  }599600  get unsupportedRendererVersionDetected(): boolean {601    return this._unsupportedRendererVersionDetected;602  }603604  get lastSelectedHostInstanceElementId(): Element['id'] | null {605    return this._lastSelectedHostInstanceElementId;606  }607608  containsElement(id: number): boolean {609    return this._idToElement.has(id);610  }611612  getElementAtIndex(index: number): Element | null {613    if (index < 0 || index >= this.numElements) {614      console.warn(615        `Invalid index ${index} specified; store contains ${this.numElements} items.`,616      );617618      return null;619    }620621    // Find which root this element is in...622    let root;623    let rootWeight = 0;624    for (let i = 0; i < this._roots.length; i++) {625      const rootID = this._roots[i];626      root = this._idToElement.get(rootID);627628      if (root === undefined) {629        // We should never reach this. This is a bug in the backend renderer.630        return this._throwAndEmitError(631          Error(632            `Couldn't find root with id "${rootID}": no matching node was found in the Store.`,633          ),634        );635      }636637      if (root.children.length === 0) {638        continue;639      }640641      if (rootWeight + root.weight > index) {642        break;643      } else {644        rootWeight += root.weight;645      }646    }647648    if (root === undefined) {649      return this._throwAndEmitError(650        Error(`Could not find an element at index "${index}" in the Store.`),651      );652    }653654    // Find the element in the tree using the weight of each node...655    // Skip over the root itself, because roots aren't visible in the Elements tree.656    let currentElement: Element = root;657    let currentWeight = rootWeight - 1;658659    while (index !== currentWeight) {660      const numChildren = currentElement.children.length;661      let didFindChild = false;662      for (let i = 0; i < numChildren; i++) {663        const childID = currentElement.children[i];664        const child = this._idToElement.get(childID);665666        if (child === undefined) {667          // We should never reach this. This is a bug in the backend renderer.668          return this._throwAndEmitError(669            Error(670              `Couldn't child element with id "${childID}": no matching node was found in the Store.`,671            ),672          );673        }674675        const childWeight = child.isCollapsed ? 1 : child.weight;676677        if (index <= currentWeight + childWeight) {678          currentWeight++;679          currentElement = child;680          didFindChild = true;681          break;682        } else {683          currentWeight += childWeight;684        }685      }686687      if (!didFindChild) {688        return this._throwAndEmitError(689          Error(690            `Could not find an element at index "${index}" because the Store tree weights are invalid.`,691          ),692        );693      }694    }695696    return currentElement;697  }698699  getElementIDAtIndex(index: number): number | null {700    const element = this.getElementAtIndex(index);701    return element === null ? null : element.id;702  }703704  getElementByID(id: number): Element | null {705    const element = this._idToElement.get(id);706    if (element === undefined) {707      console.warn(`No element found with id "${id}"`);708      return null;709    }710711    return element;712  }713714  _getElementByIDOrThrow(id: Element['id']): Element {715    const element = this._idToElement.get(id);716    if (element === undefined) {717      return this._throwAndEmitError(718        Error(719          `Could not find element with id "${id}": no matching node was found in the Store.`,720        ),721      );722    }723    return element;724  }725726  _recalculateWeightAcrossRoots(): void {727    let weightAcrossRoots = 0;728    this._roots.forEach(rootID => {729      weightAcrossRoots += this._getElementByIDOrThrow(rootID).weight;730    });731    this._weightAcrossRoots = weightAcrossRoots;732  }733734  containsSuspense(id: SuspenseNode['id']): boolean {735    return this._idToSuspense.has(id);736  }737738  getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {739    const suspense = this._idToSuspense.get(id);740    if (suspense === undefined) {741      console.warn(`No suspense found with id "${id}"`);742      return null;743    }744745    return suspense;746  }747748  // Returns a tuple of [id, index]749  getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples {750    if (!this._shouldShowWarningsAndErrors) {751      return [];752    }753754    if (this._cachedErrorAndWarningTuples !== null) {755      return this._cachedErrorAndWarningTuples;756    }757758    const errorAndWarningTuples: ErrorAndWarningTuples = [];759760    this._errorsAndWarnings.forEach((_, id) => {761      const index = this.getIndexOfElementID(id);762      if (index !== null) {763        let low = 0;764        let high = errorAndWarningTuples.length;765        while (low < high) {766          const mid = (low + high) >> 1;767          if (errorAndWarningTuples[mid].index > index) {768            high = mid;769          } else {770            low = mid + 1;771          }772        }773774        errorAndWarningTuples.splice(low, 0, {id, index});775      }776    });777778    // Cache for later (at least until the tree changes again).779    this._cachedErrorAndWarningTuples = errorAndWarningTuples;780    return errorAndWarningTuples;781  }782783  getErrorAndWarningCountForElementID(id: number): {784    errorCount: number,785    warningCount: number,786  } {787    if (!this._shouldShowWarningsAndErrors) {788      return {errorCount: 0, warningCount: 0};789    }790791    return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};792  }793794  getIndexOfElementID(id: number): number | null {795    const element = this.getElementByID(id);796797    if (element === null || element.parentID === 0) {798      return null;799    }800801    // Walk up the tree to the root.802    // Increment the index by one for each node we encounter,803    // and by the weight of all nodes to the left of the current one.804    // This should be a relatively fast way of determining the index of a node within the tree.805    let previousID = id;806    let currentID = element.parentID;807    let index = 0;808    while (true) {809      const current = this._idToElement.get(currentID);810      if (current === undefined) {811        return null;812      }813814      const {children} = current;815      for (let i = 0; i < children.length; i++) {816        const childID = children[i];817        if (childID === previousID) {818          break;819        }820821        const child = this._idToElement.get(childID);822        if (child === undefined) {823          return null;824        }825826        index += child.isCollapsed ? 1 : child.weight;827      }828829      if (current.parentID === 0) {830        // We found the root; stop crawling.831        break;832      }833834      index++;835836      previousID = current.id;837      currentID = current.parentID;838    }839840    // At this point, the current ID is a root (from the previous loop).841    // We also need to offset the index by previous root weights.842    for (let i = 0; i < this._roots.length; i++) {843      const rootID = this._roots[i];844      if (rootID === currentID) {845        break;846      }847848      const root = this._idToElement.get(rootID);849      if (root === undefined) {850        return null;851      }852853      index += root.weight;854    }855856    return index;857  }858859  isDescendantOf(parentId: number, descendantId: number): boolean {860    if (descendantId === 0) {861      return false;862    }863864    const descendant = this.getElementByID(descendantId);865    if (descendant === null) {866      return false;867    }868869    if (descendant.parentID === parentId) {870      return true;871    }872873    const parent = this.getElementByID(parentId);874    if (!parent || parent.depth >= descendant.depth) {875      return false;876    }877878    return this.isDescendantOf(parentId, descendant.parentID);879  }880881  /**882   * Returns index of the lowest descendant element, if available.883   * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective.884   */885  getIndexOfLowestDescendantElement(element: Element): number | null {886    let current: null | Element = element;887    while (current !== null) {888      if (current.isCollapsed || current.children.length === 0) {889        if (current === element) {890          return null;891        }892893        return this.getIndexOfElementID(current.id);894      } else {895        const lastChildID = current.children[current.children.length - 1];896        current = this.getElementByID(lastChildID);897      }898    }899900    return null;901  }902903  getOwnersListForElement(ownerID: number): Array<Element> {904    const list: Array<Element> = [];905    const element = this._idToElement.get(ownerID);906    if (element !== undefined) {907      list.push({908        ...element,909        depth: 0,910      });911912      const unsortedIDs = this._ownersMap.get(ownerID);913      if (unsortedIDs !== undefined) {914        const depthMap: Map<number, number> = new Map([[ownerID, 0]]);915916        // Items in a set are ordered based on insertion.917        // This does not correlate with their order in the tree.918        // So first we need to order them.919        // I wish we could avoid this sorting operation; we could sort at insertion time,920        // but then we'd have to pay sorting costs even if the owners list was never used.921        // Seems better to defer the cost, since the set of ids is probably pretty small.922        const sortedIDs = Array.from(unsortedIDs).sort(923          (idA, idB) =>924            (this.getIndexOfElementID(idA) || 0) -925            (this.getIndexOfElementID(idB) || 0),926        );927928        // Next we need to determine the appropriate depth for each element in the list.929        // The depth in the list may not correspond to the depth in the tree,930        // because the list has been filtered to remove intermediate components.931        // Perhaps the easiest way to do this is to walk up the tree until we reach either:932        // (1) another node that's already in the tree, or (2) the root (owner)933        // at which point, our depth is just the depth of that node plus one.934        sortedIDs.forEach(id => {935          const innerElement = this._idToElement.get(id);936          if (innerElement !== undefined) {937            let parentID = innerElement.parentID;938939            let depth = 0;940            while (parentID > 0) {941              if (parentID === ownerID || unsortedIDs.has(parentID)) {942                const parentDepth = depthMap.get(parentID);943                if (parentDepth === undefined) {944                  return this._throwAndEmitError(945                    Error(946                      `Invalid owners list: owner depth for element "${parentID}" was not found.`,947                    ),948                  );949                }950                depth = parentDepth + 1;951                depthMap.set(id, depth);952                break;953              }954              const parent = this._idToElement.get(parentID);955              if (parent === undefined) {956                break;957              }958              parentID = parent.parentID;959            }960961            if (depth === 0) {962              this._throwAndEmitError(Error('Invalid owners list'));963            }964965            list.push({...innerElement, depth});966          }967        });968      }969    }970971    return list;972  }973974  getSuspenseLineage(975    suspenseID: SuspenseNode['id'],976  ): $ReadOnlyArray<SuspenseNode['id']> {977    const lineage: Array<SuspenseNode['id']> = [];978    let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);979    while (next !== null) {980      if (next.parentID === 0) {981        next = null;982      } else {983        lineage.unshift(next.id);984        next = this.getSuspenseByID(next.parentID);985      }986    }987988    return lineage;989  }990991  /**992   * Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.993   */994  getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {995    let current = this._idToSuspense.get(id);996    while (current !== undefined) {997      if (current.parentID === 0) {998        return current.id;999      } else {1000        current = this._idToSuspense.get(current.parentID);1001      }1002    }1003    return null;1004  }10051006  /**1007   * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders1008   */1009  getSuspendableDocumentOrderSuspenseInitialPaint(1010    uniqueSuspendersOnly: boolean,1011  ): Array<SuspenseTimelineStep> {1012    const target: Array<SuspenseTimelineStep> = [];1013    const roots = this.roots;1014    let rootStep: null | SuspenseTimelineStep = null;1015    for (let i = 0; i < roots.length; i++) {1016      const rootID = roots[i];1017      this._getElementByIDOrThrow(rootID);1018      const rendererID = this._rootIDToRendererID.get(rootID);1019      if (rendererID === undefined) {1020        return this._throwAndEmitError(1021          Error(1022            'Failed to find renderer ID for root. This is a bug in React DevTools.',1023          ),1024        );1025      }1026      // TODO: This includes boundaries that can't be suspended due to no support from the renderer.10271028      const suspense = this.getSuspenseByID(rootID);1029      if (suspense !== null) {1030        const environments = suspense.environments;1031        const environmentName =1032          environments.length > 01033            ? environments[environments.length - 1]1034            : null;1035        if (rootStep === null) {1036          // Arbitrarily use the first root as the root step id.1037          rootStep = {1038            id: suspense.id,1039            environment: environmentName,1040            endTime: suspense.endTime,1041            rendererID,1042          };1043          target.push(rootStep);1044        } else {1045          if (rootStep.environment === null) {1046            // If any root has an environment name, then let's use it.1047            rootStep.environment = environmentName;1048          }1049          if (suspense.endTime > rootStep.endTime) {1050            // If any root has a higher end time, let's use that.1051            rootStep.endTime = suspense.endTime;1052          }1053        }1054        this.pushTimelineStepsInDocumentOrder(1055          suspense.children,1056          target,1057          uniqueSuspendersOnly,1058          environments,1059          0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.1060          rendererID,1061        );1062      }1063    }10641065    return target;1066  }10671068  _pushSuspenseChildrenInDocumentOrder(1069    children: Array<Element['id']>,1070    target: Array<SuspenseNode['id']>,1071  ): void {1072    for (let i = 0; i < children.length; i++) {1073      const childID = children[i];1074      const suspense = this._idToSuspense.get(childID);1075      if (suspense !== undefined) {1076        target.push(suspense.id);1077      } else {1078        const childElement = this._idToElement.get(childID);1079        if (childElement !== undefined) {1080          this._pushSuspenseChildrenInDocumentOrder(1081            childElement.children,1082            target,1083          );1084        }1085      }1086    }1087  }10881089  getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {1090    const transitionChildren: Array<SuspenseNode['id']> = [];10911092    const root = this._idToElement.get(id);1093    if (root === undefined) {1094      return transitionChildren;1095    }10961097    this._pushSuspenseChildrenInDocumentOrder(1098      root.children,1099      transitionChildren,1100    );11011102    return transitionChildren;1103  }11041105  /**1106   * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders1107   */1108  getSuspendableDocumentOrderSuspenseTransition(1109    uniqueSuspendersOnly: boolean,1110    rendererID: number,1111  ): Array<SuspenseTimelineStep> {1112    const target: Array<SuspenseTimelineStep> = [];1113    const focusedTransitionID = this._focusedTransition;1114    if (focusedTransitionID === 0) {1115      return this._throwAndEmitError(1116        Error(1117          'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',1118        ),1119      );1120    }11211122    target.push({1123      id: focusedTransitionID,1124      // TODO: Get environment for Activity1125      environment: null,1126      endTime: 0,1127      rendererID,1128    });11291130    const transitionChildren = this.getSuspenseChildren(focusedTransitionID);11311132    this.pushTimelineStepsInDocumentOrder(1133      transitionChildren,1134      target,1135      uniqueSuspendersOnly,1136      // TODO: Get environment for Activity1137      [],1138      0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.1139      rendererID,1140    );11411142    return target;1143  }11441145  pushTimelineStepsInDocumentOrder(1146    children: Array<SuspenseNode['id']>,1147    target: Array<SuspenseTimelineStep>,1148    uniqueSuspendersOnly: boolean,1149    parentEnvironments: Array<string>,1150    parentEndTime: number,1151    rendererID: number,1152  ): void {1153    for (let i = 0; i < children.length; i++) {1154      const child = this.getSuspenseByID(children[i]);1155      if (child === null) {1156        continue;1157      }1158      // Ignore any suspense boundaries that has no visual representation as this is not1159      // part of the visible loading sequence.1160      // TODO: Consider making visible meta data and other side-effects get virtual rects.1161      const hasRects =1162        child.rects !== null &&1163        child.rects.length > 0 &&1164        child.rects.some(isNonZeroRect);1165      const childEnvironments = child.environments;1166      // Since children are blocked on the parent, they're also blocked by the parent environments.1167      // Only if we discover a novel environment do we add that and it becomes the name we use.1168      const unionEnvironments = unionOfTwoArrays(1169        parentEnvironments,1170        childEnvironments,1171      );1172      const environmentName =1173        unionEnvironments.length > 01174          ? unionEnvironments[unionEnvironments.length - 1]1175          : null;1176      // The end time of a child boundary can in effect never be earlier than its parent even if1177      // everything unsuspended before that.1178      const maxEndTime =1179        parentEndTime > child.endTime ? parentEndTime : child.endTime;1180      if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {1181        target.push({1182          id: child.id,1183          environment: environmentName,1184          endTime: maxEndTime,1185          rendererID,1186        });1187      }1188      this.pushTimelineStepsInDocumentOrder(1189        child.children,1190        target,1191        uniqueSuspendersOnly,1192        unionEnvironments,1193        maxEndTime,1194        rendererID,1195      );1196    }1197  }11981199  getEndTimeOrDocumentOrderSuspense(1200    uniqueSuspendersOnly: boolean,1201  ): $ReadOnlyArray<SuspenseTimelineStep> {1202    let timeline: SuspenseTimelineStep[];1203    if (this._focusedTransition === 0) {1204      timeline =1205        this.getSuspendableDocumentOrderSuspenseInitialPaint(1206          uniqueSuspendersOnly,1207        );1208    } else {1209      const focusedTransitionRootID = this.getRootIDForElement(1210        this._focusedTransition,1211      );1212      if (focusedTransitionRootID === null) {1213        return this._throwAndEmitError(1214          Error(1215            'Failed to find root ID for focused transition. This is a bug in React DevTools.',1216          ),1217        );1218      }1219      const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);1220      if (rendererID === undefined) {1221        return this._throwAndEmitError(1222          Error(1223            'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',1224          ),1225        );1226      }1227      timeline = this.getSuspendableDocumentOrderSuspenseTransition(1228        uniqueSuspendersOnly,1229        rendererID,1230      );1231    }12321233    if (timeline.length === 0) {1234      return timeline;1235    }1236    const root = timeline[0];1237    // We mutate in place since we assume we've got a fresh array.1238    timeline.sort((a, b) => {1239      // Root is always first1240      return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;1241    });1242    return timeline;1243  }12441245  getActivities(): Array<{id: Element['id'], depth: number}> {1246    const target: Array<{id: Element['id'], depth: number}> = [];1247    // TODO: Keep a live tree in the backend so we don't need to recalculate1248    // this each time while also including filtered Activities.1249    this._pushActivitiesInDocumentOrder(this.roots, target, 0);1250    return target;1251  }12521253  _pushActivitiesInDocumentOrder(1254    children: $ReadOnlyArray<Element['id']>,1255    target: Array<{id: Element['id'], depth: number}>,1256    depth: number,1257  ): void {1258    for (let i = 0; i < children.length; i++) {1259      const child = this._idToElement.get(children[i]);1260      if (child === undefined) {1261        continue;1262      }1263      if (child.type === ElementTypeActivity && child.nameProp !== null) {1264        target.push({id: child.id, depth});1265        this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);1266      } else {1267        this._pushActivitiesInDocumentOrder(child.children, target, depth);1268      }1269    }1270  }12711272  getRendererIDForElement(id: number): number | null {1273    let current = this._idToElement.get(id);1274    while (current !== undefined) {1275      if (current.parentID === 0) {1276        const rendererID = this._rootIDToRendererID.get(current.id);1277        return rendererID == null ? null : rendererID;1278      } else {1279        current = this._idToElement.get(current.parentID);1280      }1281    }1282    return null;1283  }12841285  getRootIDForElement(id: number): number | null {1286    let current = this._idToElement.get(id);1287    while (current !== undefined) {1288      if (current.parentID === 0) {1289        return current.id;1290      } else {1291        current = this._idToElement.get(current.parentID);1292      }1293    }1294    return null;1295  }12961297  isInsideCollapsedSubTree(id: number): boolean {1298    let current = this._idToElement.get(id);1299    while (current != null) {1300      if (current.parentID === 0) {1301        return false;1302      } else {1303        current = this._idToElement.get(current.parentID);1304        if (current != null && current.isCollapsed) {1305          return true;1306        }1307      }1308    }1309    return false;1310  }13111312  // TODO Maybe split this into two methods: expand() and collapse()1313  toggleIsCollapsed(id: number, isCollapsed: boolean): void {1314    let didMutate = false;13151316    const element = this.getElementByID(id);1317    if (element !== null) {1318      if (isCollapsed) {1319        if (element.type === ElementTypeRoot) {1320          this._throwAndEmitError(Error('Root nodes cannot be collapsed'));1321        }13221323        if (!element.isCollapsed) {1324          didMutate = true;1325          element.isCollapsed = true;13261327          const weightDelta = 1 - element.weight;13281329          let parentElement = this._idToElement.get(element.parentID);1330          while (parentElement !== undefined) {1331            // We don't need to break on a collapsed parent in the same way as the expand case below.1332            // That's because collapsing a node doesn't "bubble" and affect its parents.1333            parentElement.weight += weightDelta;1334            parentElement = this._idToElement.get(parentElement.parentID);1335          }1336        }1337      } else {1338        let currentElement: ?Element = element;1339        while (currentElement != null) {1340          const oldWeight = currentElement.isCollapsed1341            ? 11342            : currentElement.weight;13431344          if (currentElement.isCollapsed) {1345            didMutate = true;1346            currentElement.isCollapsed = false;13471348            const newWeight = currentElement.isCollapsed1349              ? 11350              : currentElement.weight;1351            const weightDelta = newWeight - oldWeight;13521353            let parentElement = this._idToElement.get(currentElement.parentID);1354            while (parentElement !== undefined) {1355              parentElement.weight += weightDelta;1356              if (parentElement.isCollapsed) {1357                // It's important to break on a collapsed parent when expanding nodes.1358                // That's because expanding a node "bubbles" up and expands all parents as well.1359                // Breaking in this case prevents us from over-incrementing the expanded weights.1360                break;1361              }1362              parentElement = this._idToElement.get(parentElement.parentID);1363            }1364          }13651366          currentElement =1367            currentElement.parentID !== 01368              ? this.getElementByID(currentElement.parentID)1369              : null;1370        }1371      }13721373      // Only re-calculate weights and emit an "update" event if the store was mutated.1374      if (didMutate) {1375        this._recalculateWeightAcrossRoots();13761377        // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.1378        // In this  case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).1379        // Updating the selected search index later may require auto-expanding a collapsed subtree though.1380        this.emit('mutated', [[], new Map(), null]);1381      }1382    }1383  }13841385  _adjustParentTreeWeight: (1386    parentElement: ?Element,1387    weightDelta: number,1388  ) => void = (parentElement, weightDelta) => {1389    let isInsideCollapsedSubTree = false;13901391    while (parentElement != null) {1392      parentElement.weight += weightDelta;13931394      // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.1395      // Their weight will bubble up when the parent is expanded.1396      if (parentElement.isCollapsed) {1397        isInsideCollapsedSubTree = true;1398        break;1399      }14001401      parentElement = this._idToElement.get(parentElement.parentID);1402    }14031404    // Additions and deletions within a collapsed subtree should not affect the overall number of elements.1405    if (!isInsideCollapsedSubTree) {1406      this._weightAcrossRoots += weightDelta;1407    }1408  };14091410  _recursivelyUpdateSubtree(1411    id: number,1412    callback: (element: Element) => void,1413  ): void {1414    const element = this._idToElement.get(id);1415    if (element) {1416      callback(element);14171418      element.children.forEach(child =>1419        this._recursivelyUpdateSubtree(child, callback),1420      );1421    }1422  }14231424  onBridgeNativeStyleEditorSupported: ({1425    isSupported: boolean,1426    validAttributes: ?$ReadOnlyArray<string>,1427  }) => void = ({isSupported, validAttributes}) => {1428    this._isNativeStyleEditorSupported = isSupported;1429    this._nativeStyleEditorValidAttributes = validAttributes || null;14301431    this.emit('supportsNativeStyleEditor');1432  };14331434  onBridgeOperations: (operations: Array<number>) => void = operations => {1435    // $FlowFixMe[constant-condition]1436    if (__DEBUG__) {1437      console.groupCollapsed('onBridgeOperations');1438      debug('onBridgeOperations', operations.join(','));1439    }14401441    let haveRootsChanged = false;1442    let haveErrorsOrWarningsChanged = false;1443    let hasSuspenseTreeChanged = false;14441445    // The first two values are always rendererID and rootID1446    const rendererID = operations[0];14471448    const addedElementIDs: Array<number> = [];1449    // This is a mapping of removed ID -> parent ID:1450    // We'll use the parent ID to adjust selection if it gets deleted.1451    const removedElementIDs: Map<number, number> = new Map();1452    const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =1453      new Map();1454    let nextActivitySliceID: Element['id'] | null = null;14551456    let i = 2;14571458    // Reassemble the string table.1459    const stringTable: Array<string | null> = [1460      null, // ID = 0 corresponds to the null string.1461    ];1462    const stringTableSize = operations[i];1463    i++;14641465    const stringTableEnd = i + stringTableSize;14661467    while (i < stringTableEnd) {1468      const nextLength = operations[i];1469      i++;14701471      const nextString = utfDecodeStringWithRanges(1472        operations,1473        i,1474        i + nextLength - 1,1475      );1476      stringTable.push(nextString);1477      i += nextLength;1478    }14791480    while (i < operations.length) {1481      const operation = operations[i];1482      switch (operation) {1483        case TREE_OPERATION_ADD: {1484          const id = operations[i + 1];1485          const rawType = operations[i + 2];1486          const type = parseElementType(rawType);14871488          if (type === null) {1489            return this._throwAndEmitError(1490              Error(1491                `Cannot add node "${id}" because "${rawType}" is not a valid element type.`,1492              ),1493            );1494          }14951496          i += 3;14971498          if (this._idToElement.has(id)) {1499            // We should never reach this. This is a bug in the backend renderer.1500            return this._throwAndEmitError(1501              Error(1502                `Cannot add node "${id}" because a node with that id is already in the Store.`,1503              ),1504            );1505          }15061507          if (type === ElementTypeRoot) {1508            // $FlowFixMe[constant-condition]1509            if (__DEBUG__) {1510              debug('Add', `new root node ${id}`);1511            }15121513            const isStrictModeCompliant = operations[i] > 0;1514            i++;15151516            const profilerFlags = operations[i++];1517            const supportsBasicProfiling =1518              (profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;1519            const supportsPerformanceTracks =1520              (profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;1521            let supportsAdvancedProfiling: AdvancedProfiling =1522              ADVANCED_PROFILING_NONE;1523            if (supportsPerformanceTracks) {1524              supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS;1525            }15261527            let supportsStrictMode = false;1528            let hasOwnerMetadata = false;15291530            // If we don't know the bridge protocol, guess that we're dealing with the latest.1531            // If we do know it, we can take it into consideration when parsing operations.1532            if (1533              this._bridgeProtocol === null ||1534              this._bridgeProtocol.version >= 21535            ) {1536              supportsStrictMode = operations[i] > 0;1537              i++;15381539              hasOwnerMetadata = operations[i] > 0;1540              i++;1541            }15421543            this._roots = this._roots.concat(id);1544            this._rootIDToRendererID.set(id, rendererID);1545            this._rootIDToCapabilities.set(id, {1546              supportsBasicProfiling,1547              hasOwnerMetadata,1548              supportsStrictMode,1549              supportsAdvancedProfiling,1550            });15511552            // Not all roots support StrictMode;1553            // don't flag a root as non-compliant unless it also supports StrictMode.1554            const isStrictModeNonCompliant =1555              !isStrictModeCompliant && supportsStrictMode;15561557            this._idToElement.set(id, {1558              children: [],1559              depth: -1,1560              displayName: null,1561              hocDisplayNames: null,1562              id,1563              isCollapsed: false, // Never collapse roots; it would hide the entire tree.1564              isStrictModeNonCompliant,1565              isActivityHidden: false,1566              isInsideHiddenActivity: false,1567              key: null,1568              nameProp: null,1569              ownerID: 0,1570              parentID: 0,1571              type,1572              weight: 0,1573              compiledWithForget: false,1574            });15751576            haveRootsChanged = true;1577          } else {1578            const parentID = operations[i];1579            i++;15801581            const ownerID = operations[i];1582            i++;15831584            const displayNameStringID = operations[i];1585            const displayName = stringTable[displayNameStringID];1586            i++;15871588            const keyStringID = operations[i];1589            const key = stringTable[keyStringID];1590            i++;15911592            const namePropStringID = operations[i];1593            const nameProp = stringTable[namePropStringID];1594            i++;15951596            // $FlowFixMe[constant-condition]1597            if (__DEBUG__) {1598              debug(1599                'Add',1600                `node ${id} (${displayName || 'null'}) as child of ${parentID}`,1601              );1602            }16031604            const parentElement = this._idToElement.get(parentID);1605            if (parentElement === undefined) {1606              // We should never reach this. This is a bug in the backend renderer.1607              return this._throwAndEmitError(1608                Error(1609                  `Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,1610                ),1611              );1612            }16131614            parentElement.children.push(id);16151616            const {1617              formattedDisplayName: displayNameWithoutHOCs,1618              hocDisplayNames,1619              compiledWithForget,1620            } = parseElementDisplayNameFromBackend(displayName, type);16211622            const elementDepth = parentElement.depth + 1;1623            this._maximumRecordedDepth = Math.max(1624              this._maximumRecordedDepth,1625              elementDepth,1626            );16271628            const element: Element = {1629              children: [],1630              depth: elementDepth,1631              displayName: displayNameWithoutHOCs,1632              hocDisplayNames,1633              id,1634              isCollapsed: this._collapseNodesByDefault,1635              isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,1636              isActivityHidden: false,1637              isInsideHiddenActivity:1638                parentElement.isInsideHiddenActivity ||1639                parentElement.isActivityHidden,1640              key,1641              nameProp,1642              ownerID,1643              parentID,1644              type,1645              weight: 1,1646              compiledWithForget,1647            };16481649            this._idToElement.set(id, element);1650            addedElementIDs.push(id);1651            this._adjustParentTreeWeight(parentElement, 1);16521653            if (ownerID > 0) {1654              let set = this._ownersMap.get(ownerID);1655              if (set === undefined) {1656                set = new Set();1657                this._ownersMap.set(ownerID, set);1658              }1659              set.add(id);1660            }16611662            const suspense = this._idToSuspense.get(id);1663            if (suspense !== undefined) {1664              // We're reconnecting a node.1665              if (suspense.name === null) {1666                suspense.name = this._guessSuspenseName(element);1667              }1668            }1669          }1670          break;1671        }1672        case TREE_OPERATION_REMOVE: {1673          const removeLength = operations[i + 1];1674          i += 2;16751676          for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {1677            const id = operations[i];1678            const element = this._idToElement.get(id);16791680            if (element === undefined) {1681              // We should never reach this. This is a bug in the backend renderer.1682              return this._throwAndEmitError(1683                Error(1684                  `Cannot remove node "${id}" because no matching node was found in the Store.`,1685                ),1686              );1687            }16881689            i += 1;16901691            const {children, ownerID, parentID, weight} = element;1692            if (children.length > 0) {1693              // We should never reach this. This is a bug in the backend renderer.1694              return this._throwAndEmitError(1695                Error(`Node "${id}" was removed before its children.`),1696              );1697            }16981699            let parentElement: ?Element = null;1700            if (parentID === 0) {1701              // $FlowFixMe[constant-condition]1702              if (__DEBUG__) {1703                debug('Remove', `node ${id} root`);1704              }17051706              this._roots = this._roots.filter(rootID => rootID !== id);1707              this._rootIDToRendererID.delete(id);1708              this._rootIDToCapabilities.delete(id);17091710              haveRootsChanged = true;1711            } else {1712              // $FlowFixMe[constant-condition]1713              if (__DEBUG__) {1714                debug('Remove', `node ${id} from parent ${parentID}`);1715              }17161717              parentElement = this._idToElement.get(parentID);1718              if (parentElement === undefined) {1719                // We should never reach this. This is a bug in the backend renderer.1720                return this._throwAndEmitError(1721                  Error(1722                    `Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,1723                  ),1724                );1725              }17261727              const index = parentElement.children.indexOf(id);1728              if (index === -1) {1729                return this._throwAndEmitError(1730                  Error(1731                    `Cannot remove node "${id}" from parent "${parentID}" because it is not a child of the parent.`,1732                  ),1733                );1734              }1735              parentElement.children.splice(index, 1);1736            }17371738            this._idToElement.delete(id);17391740            this._adjustParentTreeWeight(parentElement, -weight);1741            removedElementIDs.set(id, parentID);17421743            this._ownersMap.delete(id);1744            if (ownerID > 0) {1745              const set = this._ownersMap.get(ownerID);1746              if (set !== undefined) {1747                set.delete(id);1748              }1749            }17501751            if (this._errorsAndWarnings.has(id)) {1752              this._errorsAndWarnings.delete(id);1753              haveErrorsOrWarningsChanged = true;1754            }1755          }17561757          break;1758        }1759        case TREE_OPERATION_REORDER_CHILDREN: {1760          const id = operations[i + 1];1761          const numChildren = operations[i + 2];1762          i += 3;17631764          const element = this._idToElement.get(id);1765          if (element === undefined) {1766            // We should never reach this. This is a bug in the backend renderer.1767            return this._throwAndEmitError(1768              Error(1769                `Cannot reorder children for node "${id}" because no matching node was found in the Store.`,1770              ),1771            );1772          }17731774          const children = element.children;1775          if (children.length !== numChildren) {1776            // We should never reach this. This is a bug in the backend renderer.1777            return this._throwAndEmitError(1778              Error(1779                `Children cannot be added or removed during a reorder operation.`,1780              ),1781            );1782          }17831784          const reorderedChildIDs: Set<Element['id']> = new Set();1785          for (let j = 0; j < numChildren; j++) {1786            const childID = operations[i + j];1787            const childElement = this._idToElement.get(childID);1788            if (1789              childElement === undefined ||1790              childElement.parentID !== id ||1791              reorderedChildIDs.has(childID)1792            ) {1793              return this._throwAndEmitError(1794                Error(1795                  `Children cannot be added or removed during a reorder operation.`,1796                ),1797              );1798            }1799            reorderedChildIDs.add(childID);1800          }1801          for (let j = 0; j < numChildren; j++) {1802            children[j] = operations[i + j];1803          }1804          i += numChildren;18051806          // $FlowFixMe[constant-condition]1807          if (__DEBUG__) {1808            debug('Re-order', `Node ${id} children ${children.join(',')}`);1809          }1810          break;1811        }1812        case TREE_OPERATION_SET_SUBTREE_MODE: {1813          const id = operations[i + 1];1814          const mode = operations[i + 2];18151816          i += 3;18171818          // If elements have already been mounted in this subtree, update them.1819          // (In practice, this likely only applies to the root element.)1820          if (mode === StrictMode) {1821            this._recursivelyUpdateSubtree(id, element => {1822              element.isStrictModeNonCompliant = false;1823            });1824          } else if (mode === ActivityHiddenMode) {1825            const element = this._idToElement.get(id);1826            if (element != null) {1827              element.isActivityHidden = true;1828              element.children.forEach(childID =>1829                this._recursivelyUpdateSubtree(childID, child => {1830                  child.isInsideHiddenActivity = true;1831                }),1832              );1833              // Collapse hidden Activity subtrees by default.1834              if (!element.isCollapsed) {1835                element.isCollapsed = true;1836                if (element.children.length > 0) {1837                  const weightDelta = 1 - element.weight;1838                  const parentElement = this._idToElement.get(element.parentID);1839                  this._adjustParentTreeWeight(parentElement, weightDelta);1840                }1841              }1842            }1843          } else if (mode === ActivityVisibleMode) {1844            const element = this._idToElement.get(id);1845            if (element != null) {1846              element.isActivityHidden = false;1847              element.children.forEach(childID =>1848                this._recursivelyUpdateSubtree(childID, child => {1849                  child.isInsideHiddenActivity = false;1850                }),1851              );1852              // Expand Activity subtree when it becomes visible.1853              if (element.isCollapsed && element.children.length > 0) {1854                element.isCollapsed = false;1855                const weightDelta = element.weight - 1;1856                const parentElement = this._idToElement.get(element.parentID);1857                this._adjustParentTreeWeight(parentElement, weightDelta);1858              }1859            }1860          }18611862          // $FlowFixMe[constant-condition]1863          if (__DEBUG__) {1864            debug(1865              'Subtree mode',1866              `Subtree with root ${id} set to mode ${mode}`,1867            );1868          }1869          break;1870        }1871        case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:1872          // Base duration updates are only sent while profiling is in progress.1873          // We can ignore them at this point.1874          // The profiler UI uses them lazily in order to generate the tree.1875          i += 3;1876          break;1877        case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {1878          const id = operations[i + 1];1879          const errorCount = operations[i + 2];1880          const warningCount = operations[i + 3];18811882          i += 4;18831884          if (errorCount > 0 || warningCount > 0) {1885            this._errorsAndWarnings.set(id, {errorCount, warningCount});1886          } else if (this._errorsAndWarnings.has(id)) {1887            this._errorsAndWarnings.delete(id);1888          }1889          haveErrorsOrWarningsChanged = true;1890          break;1891        }1892        case SUSPENSE_TREE_OPERATION_ADD: {1893          const id = operations[i + 1];1894          const parentID = operations[i + 2];1895          const nameStringID = operations[i + 3];1896          const isSuspended = operations[i + 4] === 1;1897          const numRects = operations[i + 5];1898          let name = stringTable[nameStringID];18991900          if (this._idToSuspense.has(id)) {1901            // We should never reach this. This is a bug in the backend renderer.1902            return this._throwAndEmitError(1903              Error(1904                `Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,1905              ),1906            );1907          }19081909          const element = this._idToElement.get(id);1910          if (element === undefined) {1911            // This element isn't connected yet.1912          } else {1913            if (name === null) {1914              // The boundary isn't explicitly named.1915              // Pick a sensible default.1916              if (parentID === 0) {1917                // For Roots we use their display name.1918                name = element.displayName;1919              } else {1920                name = this._guessSuspenseName(element);1921              }1922            }1923          }19241925          i += 6;1926          let rects: SuspenseNode['rects'];1927          if (numRects === -1) {1928            rects = null;1929          } else {1930            rects = [];1931            for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {1932              const x = operations[i + 0] / 1000;1933              const y = operations[i + 1] / 1000;1934              const width = operations[i + 2] / 1000;1935              const height = operations[i + 3] / 1000;1936              const rect = {x, y, width, height};1937              if (parentID !== 0) {1938                // Track all rects except the root.1939                this._rtree.insert(rect);1940              }1941              rects.push(rect);1942              i += 4;1943            }1944          }19451946          // $FlowFixMe[constant-condition]1947          if (__DEBUG__) {1948            debug('Suspense Add', `node ${id} as child of ${parentID}`);1949          }19501951          if (parentID !== 0) {1952            const parentSuspense = this._idToSuspense.get(parentID);1953            if (parentSuspense === undefined) {1954              // We should never reach this. This is a bug in the backend renderer.1955              return this._throwAndEmitError(1956                Error(1957                  `Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,1958                ),1959              );1960            }19611962            parentSuspense.children.push(id);1963          }19641965          this._idToSuspense.set(id, {1966            id,1967            parentID,1968            children: [],1969            name,1970            rects,1971            hasUniqueSuspenders: false,1972            isSuspended: isSuspended,1973            environments: [],1974            endTime: 0,1975          });19761977          hasSuspenseTreeChanged = true;1978          break;1979        }1980        case SUSPENSE_TREE_OPERATION_REMOVE: {1981          const removeLength = operations[i + 1];1982          i += 2;19831984          for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {1985            const id = operations[i];1986            const suspense = this._idToSuspense.get(id);19871988            if (suspense === undefined) {1989              // We should never reach this. This is a bug in the backend renderer.1990              return this._throwAndEmitError(1991                Error(1992                  `Cannot remove suspense node "${id}" because no matching node was found in the Store.`,1993                ),1994              );1995            }19961997            i += 1;19981999            const {children, parentID, rects} = suspense;2000            if (children.length > 0) {

Code quality findings 100

Remove debugging statements or use a logging library
info correctness console-log
console.log(
Ensure all cases are handled or a default case is present
info correctness switch-without-default
switch (type) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (config != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
isProfiling = config.isProfiling === true;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (this.roots.length === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (map.size !== expectedSize) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (this._hookSettings === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (root === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (root.children.length === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (root === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (index !== currentWeight) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
return element === null ? null : element.id;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (suspense === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (this._cachedErrorAndWarningTuples !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (index !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element === null || element.parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (childID === previousID) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rootID === currentID) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (root === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (descendantId === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (descendant === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (descendant.parentID === parentId) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (current !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.isCollapsed || current.children.length === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current === element) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (unsortedIDs !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (innerElement !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parentID === ownerID || unsortedIDs.has(parentID)) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parentDepth === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parent === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (depth === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (next !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (next.parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (current !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rendererID === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (suspense !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rootStep === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rootStep.environment === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (suspense !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (childElement !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (root === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (focusedTransitionID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.rects !== null &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (this._focusedTransition === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (focusedTransitionRootID === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rendererID === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (timeline.length === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.type === ElementTypeActivity && child.nameProp !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (current !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
return rendererID == null ? null : rendererID;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (current !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.parentID === 0) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
while (current != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (current.parentID === 0) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (current != null && current.isCollapsed) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element.type === ElementTypeRoot) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (parentElement !== undefined) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
while (currentElement != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (parentElement !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
currentElement.parentID !== 0
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
while (parentElement != null) {
Ensure all cases are handled or a default case is present
info correctness switch-without-default
switch (operation) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (type === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (type === ElementTypeRoot) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
this._bridgeProtocol === null ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parentElement === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (set === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (suspense !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (suspense.name === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parentID === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
this._roots = this._roots.filter(rootID => rootID !== id);
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (parentElement === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (index === -1) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (set !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (element === undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (children.length !== numChildren) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
childElement === undefined ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
childElement.parentID !== id ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (mode === StrictMode) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (mode === ActivityHiddenMode) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (element != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (mode === ActivityVisibleMode) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (element != null) {

Get this view in your editor

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