Skip to content

Commit

Permalink
Improve hook path capturing (fixes #30)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed Aug 14, 2023
1 parent d2e7901 commit e6c9bfa
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 33 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## next

- Improved hook path capturing:
- Adjusted the stack trace limit to capture up to 25 entries, guaranteeing a minimum of 20 path entries, an improvement from the previous 6 (fixes #30)
- Implemented an alternative method for path extraction to cater to scenarios where a function is either unnamed or its name doesn't start with `use`

## 0.7.3 (June 9, 2023)

- Added `StrictMode` capturing and displaying in components tree
Expand Down
21 changes: 21 additions & 0 deletions playground/cases/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,34 @@ function Root() {
}

function useFoo() {
badNameHook();
evalHook();
anonymousHook();
anonymousHookFactory();
return useBar();
}

function useBar() {
const [, setState] = React.useState(0);

React.useEffect(() => {
setState(1);
}, []);

return React.useContext(CtxA);
}

function badNameHook() {
return useBar();
}

const evalHook = new Function("useBar", "return () => useBar()")(useBar);
const anonymousHook = () => useBar();
const anonymousHookFactory = (
() => () =>
useBar()
)();

const Child = React.forwardRef(function Child(
{ prop = 123 }: { prop: number },
ref
Expand Down
28 changes: 13 additions & 15 deletions src/publisher/react-integration/dispatcher-trap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
HookCompute,
} from "../types";
import { CoreApi } from "./core";
import { extractCallLoc, parseStackTraceLine } from "./utils/stackTrace";
import { extractCallLoc, getParsedStackTrace } from "./utils/stackTrace";

type StateHookName = "useState" | "useReducer" | "useTransition";
type MemoHookName = "useMemo" | "useCallback";
Expand Down Expand Up @@ -43,30 +43,27 @@ type FiberDispatcherInfo = {
hooks: HookInfo[];
};

function extractHookPath(depth = 0) {
const stack = String(new Error().stack)
.split("\n")
.slice(4 + depth);
function extractHookPath(depth = 0, stopHookPathLocation: string | null) {
const stack = getParsedStackTrace(depth + 4);
const path = [];
const result: TransferCallTrace = {
path: undefined,
loc: null,
};
let prev: TransferCallTrace | TransferCallTracePoint = result;

for (const line of stack) {
const parsed = parseStackTraceLine(line);

for (const parsed of stack) {
if (!parsed) {
break;
}

prev.loc = parsed.loc;

if (!parsed.name.startsWith("use")) {
if (parsed.loc === stopHookPathLocation) {
path.shift();
path.shift();
break;
}

prev.loc = parsed.loc;
path.unshift(
(prev = {
name: parsed.name,
Expand Down Expand Up @@ -96,6 +93,7 @@ export function createDispatcherTrap(
let currentEffectName: "effect" | "layout-effect" | null = null;
let currentFiberHookIndex = 0;
let dispatchCalls: FiberDispatchCall[] = [];
let stopHookPathLocation: string | null = null;
// let currentFiberRerenderState: RerenderState | null = null;
const knownDispatcher = new Set<Dispatcher>();
const ignoreDispatcherTransition = new Set<Dispatcher>();
Expand Down Expand Up @@ -123,7 +121,7 @@ export function createDispatcherTrap(
name,
deps,
context,
trace: extractHookPath(1),
trace: extractHookPath(1, stopHookPathLocation),
});
}

Expand Down Expand Up @@ -386,9 +384,7 @@ export function createDispatcherTrap(

if (typeof renderer.getCurrentFiber === "function") {
Object.defineProperty(renderer.currentDispatcherRef, "current", {
get() {
return currentDispatcher;
},
get: () => currentDispatcher,
set(nextDispatcher: Dispatcher | null) {
const nextCurrentFiber = renderer.getCurrentFiber();
const prevDispatcher = currentDispatcher;
Expand All @@ -400,6 +396,7 @@ export function createDispatcherTrap(
currentFiber = nextCurrentFiber;
currentFiberCollectInfo = null;
currentFiberHookIndex = 0;
currentRoot = null;

if (currentFiber !== null) {
const alternate = currentFiber.alternate;
Expand All @@ -412,6 +409,7 @@ export function createDispatcherTrap(
);

if (!fiberTypeInfo.has(fiberTypeId)) {
stopHookPathLocation = extractCallLoc(1);
fiberTypeInfo.set(
fiberTypeId,
(currentFiberCollectInfo = {
Expand Down
87 changes: 69 additions & 18 deletions src/publisher/react-integration/utils/stackTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,43 @@
// https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js

const UNKNOWN_FUNCTION = "<unknown>";
const hasOwn =
Object.hasOwn ||
((target, prop) => Object.prototype.hasOwnProperty.call(target, prop));

type LineParseResult = null | {
name: string;
loc: string | null;
};

function getCallStackLine(depth: number) {
const stack = String(new Error().stack).split("\n");

return stack[stack[0] === "Error" ? depth + 1 : depth];
function lazyParseArray<T extends string | NodeJS.CallSite>(
array: T[],
parse: (val: T) => LineParseResult
) {
const cache: LineParseResult[] = [];

return new Proxy(array, {
get(target, prop) {
if (typeof prop === "string") {
const index = Number(prop);

if (isFinite(index) && hasOwn(array, prop)) {
// Check if the value at the index is already parsed
if (typeof cache[index] === "undefined") {
cache[index] = parse(array[index]);
}

return cache[index];
}
}

return (target as any)[prop];
},
});
}

export function extractCallLoc(depth: number) {
const line = getCallStackLine(depth + 3);
const parsed = line ? parseStackTraceLine(line) : null;
const parsed = getParsedStackTrace()[depth + 2];

if (parsed && parsed.loc) {
return parsed.loc;
Expand All @@ -25,22 +47,51 @@ export function extractCallLoc(depth: number) {
return null;
}

/**
* This parses the different stack traces and puts them into one format
* This borrows heavily from TraceKit (https://github.com/csnover/TraceKit)
*/
export function parseStackTrace(stackString: string) {
const lines = stackString.split("\n");
export function getParsedStackTrace(skip = 1, limit = 25) {
const prevPrepareStackTrace = Error.prepareStackTrace;
const prevStackTraceLimit = Error.stackTraceLimit;

try {
Error.stackTraceLimit = limit;
Error.prepareStackTrace = (_, callSites) => {
result = lazyParseArray(callSites.slice(skip), parseCallSite);

return "";
};

return lines.reduce((stack, line) => {
const parseResult = parseStackTraceLine(line);
let result: NodeJS.CallSite[] | string[] | null = null;
const stack = new Error().stack;

if (parseResult) {
stack.push(parseResult);
if (result === null && stack) {
const lines = stack.trim().split("\n");

result = lazyParseArray(
lines.slice(lines[0] === "Error" ? skip + 1 : skip),
parseStackTraceLine
);
}

return stack;
}, [] as LineParseResult[]);
return (result || []) as unknown as LineParseResult[]; // TS doesn't handle Proxy right
} finally {
Error.stackTraceLimit = prevStackTraceLimit;
Error.prepareStackTrace = prevPrepareStackTrace;
}
}

function parseCallSite(
callSite: NodeJS.CallSite & { getScriptNameOrSourceURL?: () => string | null }
) {
const filename =
typeof callSite.getScriptNameOrSourceURL === "function"
? callSite.getScriptNameOrSourceURL()
: callSite.getFileName();

return {
loc: filename
? `${filename}:${callSite.getLineNumber()}:${callSite.getColumnNumber()}`
: null,
name: callSite.getFunctionName() || UNKNOWN_FUNCTION,
};
}

export function parseStackTraceLine(line: string): LineParseResult {
Expand Down

0 comments on commit e6c9bfa

Please sign in to comment.