Repository: facebook/react
Pull request: [DevTools] Use source maps to infer name asynchronously

Stacked on #34210.

For inferred name of Promises, like an anonymous Promise passed to `use()` we currently take the function name of the first stack frame. https://github.com/facebook/react/pull/34162

This is usually good enough but if the Hook is in third party code then it can be a deep internal hook that you don't know anything about. This tries to skip ignore listed hooks and take the last ignore listed hook that was ignore listed. So that we get the name of the callsite into third party which is likely the name of the public API.

```js
// node_modules/foo/bar.js

function useInternalHook() {
  use(promise);
}

export function useHook() {
  return useInternalHook();
}
```

This would now briefly show the name `useInternalHook` and then switch to `useHook` after source mapping.

This also tries to take the function name from the source mapped name but currently this is always empty since our source mapping doesn't extract the function name from source. So it always fallback to the generated source's function name.


Changed files:
filename: packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
patch: @@ -20,6 +20,7 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit
 import StackTraceView from './StackTraceView';
 import OwnerView from './OwnerView';
 import {meta} from '../../../hydration';
+import useInferredName from '../useInferredName';
 
 import type {
   InspectedElement,
@@ -101,21 +102,7 @@ function SuspendedByRow({
 }: RowProps) {
   const [isOpen, setIsOpen] = useState(false);
   const ioInfo = asyncInfo.awaited;
-  let name = ioInfo.name;
-  if (name === '' || name === 'Promise') {
-    // If all we have is a generic name, we can try to infer a better name from
-    // the stack. We only do this if the stack has more than one frame since
-    // otherwise it's likely to just be the name of the component which isn't better.
-    const bestStack = ioInfo.stack || asyncInfo.stack;
-    if (bestStack !== null && bestStack.length > 1) {
-      // TODO: Ideally we'd get the name from the last ignore listed frame before the
-      // first visible frame since this is the same algorithm as the Flight server uses.
-      // Ideally, we'd also get the name from the source mapped entry instead of the
-      // original entry. However, that would require suspending the immediate display
-      // of these rows to first do source mapping before we can show the name.
-      name = bestStack[0][0];
-    }
-  }
+  const name = useInferredName(asyncInfo);
   const description = ioInfo.description;
   const longName = description === '' ? name : name + ' (' + description + ')';
   const shortDescription = getShortDescription(name, description);

filename: packages/react-devtools-shared/src/devtools/views/useInferredName.js
patch: @@ -0,0 +1,78 @@
+import {use, useContext, useDeferredValue} from 'react';
+
+import type {ReactCallSite} from 'shared/ReactTypes';
+
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
+
+import type {SerializedAsyncInfo} from 'react-devtools-shared/src/frontend/types';
+
+import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
+
+import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
+
+export default function useInferredName(
+  asyncInfo: SerializedAsyncInfo,
+): string {
+  const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
+  const name = asyncInfo.awaited.name;
+  let inferNameFromStack = null;
+  if (!name || name === 'Promise') {
+    // If all we have is a generic name, we can try to infer a better name from
+    // the stack. We only do this if the stack has more than one frame since
+    // otherwise it's likely to just be the name of the component which isn't better.
+    const bestStack = asyncInfo.awaited.stack || asyncInfo.stack;
+    if (bestStack !== null && bestStack.length > 1) {
+      inferNameFromStack = bestStack;
+    }
+  }
+  // Start by not source mapping and just taking the first name and upgrade to
+  // the better name asynchronously if we find one. Most of the time it'll just be
+  // the top of the stack.
+  const shouldSourceMap = useDeferredValue(inferNameFromStack !== null, false);
+  if (inferNameFromStack !== null) {
+    if (shouldSourceMap) {
+      let bestMatch = '';
+      for (let i = 0; i < inferNameFromStack.length; i++) {
+        const callSite: ReactCallSite = inferNameFromStack[i];
+        const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
+          callSite;
+        const symbolicatedCallSite: null | SourceMappedLocation =
+          fetchFileWithCaching !== null
+            ? use(
+                symbolicateSourceWithCache(
+                  fetchFileWithCaching,
+                  virtualURL,
+                  virtualLine,
+                  virtualColumn,
+                ),
+              )
+            : null;
+        if (symbolicatedCallSite === null) {
+          // If we can't source map, we treat it as first party code. We called whatever was
+          // the previous callsite.
+          if (bestMatch === '') {
+            return virtualFunctionName || name;
+          } else {
+            return bestMatch;
+          }
+        } else if (!symbolicatedCallSite.ignored) {
+          if (bestMatch === '') {
+            // If we had no good stack frames for internal calls, just use the last
+            // first party function name.
+            return symbolicatedCallSite[0] || virtualFunctionName || name;
+          } else {
+            return bestMatch;
+          }
+        } else {
+          // This line was ignore listed, it might be the one we called into from first party.
+          bestMatch = symbolicatedCallSite[0] || virtualFunctionName;
+        }
+      }
+      return name;
+    } else {
+      return inferNameFromStack[0][0];
+    }
+  } else {
+    return name;
+  }
+}