Skip to content

Fix crash when inspecting JSX elements with circular or non-object props - #30630

Closed
robobun wants to merge 1 commit into
mainfrom
farm/301da87b/inspect-jsx-circular
Closed

Fix crash when inspecting JSX elements with circular or non-object props#30630
robobun wants to merge 1 commit into
mainfrom
farm/301da87b/inspect-jsx-circular

Conversation

@robobun

@robobun robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a segfault in Bun.inspect() / console.log() (and the jest pretty formatter) when formatting React elements that have been mutated to contain circular references or non-object props.

Root cause

  1. The .JSX tag was not included in canHaveCircularReferences(), so the visited-set / stack check that normally short-circuits cycles was skipped for React elements. A React element whose props, key, or children referred back to itself would recurse until the native stack overflowed.
  2. The JSX formatter assumed props is always an object and did props.getObject().?, which panics when props is a primitive.

Fix

  • Add .JSX to canHaveCircularReferences() in both ConsoleObject.zig and pretty_format.zig so cycles print [Circular] instead of overflowing the stack.
  • Guard the props iterator behind props.getObject() so non-object props are simply skipped.

Repro

const el = { $$typeof: Symbol.for("react.element"), type: "div", key: null, ref: null, props: {} };
el.props = el;
Bun.inspect(el); // before: SIGSEGV, after: '<div ... props=[Circular] />'

How did you verify your code works?

Added regression tests to test/js/bun/util/inspect.test.js covering circular props, circular key, circular children, and primitive props. Existing JSX inspect tests, console tests, and md-react.test.ts continue to pass.

Found by Fuzzilli (fingerprint 13aabad674c61341).

The JSX tag was not included in canHaveCircularReferences, so
inspecting a React element whose props/key/children referred back to
itself would recurse until the stack overflowed. The JSX formatter
also unconditionally unwrapped props as an object, panicking when
props was a primitive.
@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:18 AM PT - May 13th, 2026

@robobun, your commit dbf0c2c has 2 failures in Build #54052 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30630

That installs a local version of the PR into your bun-30630 executable, so you can run:

bun-30630 --bun

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR extends JSX circular-reference detection in two formatting implementations (ConsoleObject and JestPrettyFormat), refactors JSX props handling to defensively check whether props is an object before accessing it, and adds comprehensive test coverage for circular references and non-object props in JSX inspection.

Changes

JSX Circular Reference Handling

Layer / File(s) Summary
Enable JSX circular-reference detection
src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig
The canHaveCircularReferences method now includes .JSX in both console and Jest formatters, enabling circular-reference tracking for JSX values.
Defensive JSX props handling in formatters
src/jsc/ConsoleObject.zig, src/test_runner/pretty_format.zig
JSX formatter props logic is refactored to guard on props.getObject() succeeding before accessing properties, replacing the previous unconditional assertion and safely handling non-object props values.
JSX circular reference and non-object props tests
test/js/bun/util/inspect.test.js
Four new test cases verify that Bun.inspect safely handles JSX objects with circular props, key, or children references (represented as "[Circular]"), and that non-object props values do not throw.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing a crash when inspecting JSX elements with circular or non-object props, which directly matches the core issue addressed in the changeset.
Description check ✅ Passed The PR description fully addresses both required template sections with comprehensive detail: 'What does this PR do?' explains the fixes and root causes, and 'How did you verify your code works?' describes regression tests and existing test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test_runner/pretty_format.zig`:
- Around line 329-330: The pretty formatter currently doesn't treat React 19
transitional elements as JSX because the JSX detection condition only checks
tags (.Array, .Object, .Map, .Set, .JSX); update the JSX detection logic in
src/test_runner/pretty_format.zig to also recognize
Symbol.for("react.transitional.element") the same way ConsoleObject.zig does
(mirror the transitional symbol check used around the ConsoleObject handling) so
that when the inspected value matches the transitional element symbol it will
return tag == .JSX and thus participate in the pretty printer's circular
reference/JSX handling; locate the JSX/tag-checking function (the block that
returns tag == .Array or tag == .Object or tag == .Map or tag == .Set or tag ==
.JSX) and add the transitional symbol check analogous to ConsoleObject.zig.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d223d7e-09d6-4a54-a0d4-32e49eb3d03d

📥 Commits

Reviewing files that changed from the base of the PR and between b9c757b and dbf0c2c.

📒 Files selected for processing (3)
  • src/jsc/ConsoleObject.zig
  • src/test_runner/pretty_format.zig
  • test/js/bun/util/inspect.test.js

Comment on lines +329 to 330
return tag == .Array or tag == .Object or tag == .Map or tag == .Set or tag == .JSX;
}

@coderabbitai coderabbitai Bot May 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking JSX symbol handling parity between inspect and pretty formatter..."
rg -n -C2 'react\.element|react\.transitional\.element|react\.fragment|\$\$typeof' src/jsc/ConsoleObject.zig src/test_runner/pretty_format.zig

echo
echo "Checking tests for transitional symbol coverage..."
rg -n -C2 'react\.transitional\.element|react\.element|\\$\\$typeof' test

Repository: oven-sh/bun

Length of output: 8294


Add react.transitional.element support to pretty formatter for React 19 parity

pretty_format.zig doesn't recognize Symbol.for("react.transitional.element"), creating a parity gap with ConsoleObject.zig which already handles it. This prevents React 19 transitional elements from being tagged as .JSX in Jest pretty formatting, bypassing circular reference handling for these elements.

Mirror the approach in ConsoleObject.zig (lines 1305-1310) by adding the transitional symbol check:

Fix
                         var reactElement = ZigString.init("react.element");
+                        var react_element_transitional = ZigString.init("react.transitional.element");
                         var react_fragment = ZigString.init("react.fragment");
 
-                        if (try typeof_symbol.isSameValue(.symbolFor(globalThis, &reactElement), globalThis) or try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_fragment), globalThis)) {
+                        if (try typeof_symbol.isSameValue(.symbolFor(globalThis, &reactElement), globalThis) or
+                            try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_element_transitional), globalThis) or
+                            try typeof_symbol.isSameValue(.symbolFor(globalThis, &react_fragment), globalThis))
+                        {
                             return .{ .tag = .JSX, .cell = js_type };
                         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test_runner/pretty_format.zig` around lines 329 - 330, The pretty
formatter currently doesn't treat React 19 transitional elements as JSX because
the JSX detection condition only checks tags (.Array, .Object, .Map, .Set,
.JSX); update the JSX detection logic in src/test_runner/pretty_format.zig to
also recognize Symbol.for("react.transitional.element") the same way
ConsoleObject.zig does (mirror the transitional symbol check used around the
ConsoleObject handling) so that when the inspected value matches the
transitional element symbol it will return tag == .JSX and thus participate in
the pretty printer's circular reference/JSX handling; locate the
JSX/tag-checking function (the block that returns tag == .Array or tag ==
.Object or tag == .Map or tag == .Set or tag == .JSX) and add the transitional
symbol check analogous to ConsoleObject.zig.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing parity gap unrelated to the crash being fixed here. When pretty_format.zig doesn't recognize react.transitional.element, it falls through to .Object, which already participates in circular reference detection — so there's no crash risk for that case. Keeping this PR scoped to the crash fix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robobun, that makes sense — since unrecognized react.transitional.element objects fall through to .Object, which is already in canHaveCircularReferences(), there's no crash exposure for that case. Keeping the PR scoped to the crash fix is the right call. Thanks for the clarification!


🧠 Learnings used
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Learnt from: theshadow27
Repo: oven-sh/bun PR: 26310
File: src/cache.zig:123-127
Timestamp: 2026-01-20T21:51:21.482Z
Learning: Ensure readFileShared in Zig files follows the same FD lifecycle as readFileWithAllocator: compute will_close = rfs.needToCloseFiles() and opened_file; use it in defer; and return bun.invalid_fd (not the closed handle) when will_close is true to avoid exposing closed FDs. This pattern should be applied to similar readFile* helpers across the Zig codebase to maintain correct resource lifecycle and avoid leaking or returning invalid file descriptors.

Learnt from: jimmywarting
Repo: oven-sh/bun PR: 26595
File: src/bun.js/webcore/Blob.zig:3839-3841
Timestamp: 2026-02-12T22:09:40.373Z
Learning: In Zig source files implementing Blob (e.g., Blob constructors in src/bun.js/webcore/Blob.zig), ensure that Blob construction does not synchronously read file contents. When building a Blob from parts that include file-backed blobs (e.g., Bun.file() results), create references to those blobs that preserve offset/size metadata instead of eagerly loading bytes into memory. Only the read methods (text(), arrayBuffer(), stream(), etc.) should perform actual file I/O. This aligns with Web API semantics (Blob constructor is synchronous but non-blocking) and common runtime patterns (Node.js, Deno, fetch-blob). The Blob should internally store an array of BlobParts (ArrayBuffer, file-backed Blob with offset/size, other Blob references) and materialize bytes only when read methods are invoked.

Learnt from: alii
Repo: oven-sh/bun PR: 28128
File: src/cli/test/Scanner.zig:170-178
Timestamp: 2026-03-15T05:22:35.951Z
Learning: In oven-sh/bun's Zig code, avoid flagging small fixed-size buffers as needing path_buffer_pool. Use path_buffer_pool.get()/put() only when you actually need a MAX_PATH_BYTES-sized buffer (around ~96KB on Windows). For small buffers (e.g., [4096]u8) used for short relative paths under ~1KB, allocate on the stack and avoid pool overhead on hot paths. Do not apply path_buffer_pool guidance unless a full MAX_PATH_BYTES-sized buffer is required.

Learnt from: robobun
Repo: oven-sh/bun PR: 28299
File: src/bun.js/api/cron.zig:311-315
Timestamp: 2026-03-20T16:27:36.818Z
Learning: In oven-sh/bun Zig code, treat these as equivalent and acceptable: `bun.FD.cwd().makePath(u8, path)` and `bun.makePath(bun.FD.cwd().stdDir(), path)`. The underlying implementation (`FD.makePath` around src/fd.zig) is a thin wrapper that delegates to `bun.makePath`, so reviewers should not flag `bun.FD.cwd().makePath(u8, ...)` as non-standard or incorrect when it’s used for path creation.

Learnt from: cirospaciari
Repo: oven-sh/bun PR: 28611
File: src/http.zig:565-620
Timestamp: 2026-03-30T19:17:01.758Z
Learning: In this oven-sh/bun Zig codebase, treat `bun.hash()` as a one-shot helper: it hashes a single `[]const u8` buffer and is appropriate only for single-buffer, non-incremental hashing. For incremental hashing across multiple chunks (e.g., building a hash in a loop over header name/value pairs), use the wyhash streaming API directly: `std.hash.Wyhash.init(0)`, then call `.update(chunk)` for each piece, and finally `.final()` to produce the result. Do not apply the CLAUDE.md “use `bun.hash()` for 64-bit wyhash” guideline to incremental/multi-buffer cases—use `Wyhash.init/update/final` instead even though both ultimately rely on wyhash.

Learnt from: robobun
Repo: oven-sh/bun PR: 29137
File: src/bun.js/node/node_util_binding.zig:145-164
Timestamp: 2026-04-11T03:00:34.188Z
Learning: In this repo’s Zig code (oven-sh/bun), it’s acceptable to call `std.posix.isatty(fd_int)` directly when you already have a file descriptor as a raw `i32`. Do not treat direct `std.posix.isatty(fd_int)` calls as inconsistent just because `bun.sys.File.isTty()` exists—`bun.sys.File.isTty()` is essentially `std.posix.isatty(self.handle.cast())` and would require constructing a `bun.sys.File` first. Review should not flag direct `std.posix.isatty(fd_int)` usage as a `bun.sys` pattern violation.

Learnt from: robobun
Repo: oven-sh/bun PR: 29154
File: src/interchange/xml.zig:443-445
Timestamp: 2026-04-13T17:55:27.597Z
Learning: When reviewing Zig code in oven-sh/bun, note that `bun.strings.eqlLong(a, b, comptime check_len: bool)` performs a case-sensitive equality check. The `check_len` comptime bool only controls whether a length pre-check happens before the SIMD byte comparison, and is not related to case sensitivity. Therefore, do not treat `eqlLong(a, b, true)` (or any `check_len` value) as a case-insensitive comparison; only flag for case-insensitivity if the code actually uses a case-folding/comparison API intended for that purpose.

Learnt from: robobun
Repo: oven-sh/bun PR: 29422
File: src/bun.js/api/JSBundler.zig:0-0
Timestamp: 2026-04-18T04:44:25.893Z
Learning: In oven-sh/bun Zig code, follow the established OutOfMemory handling pattern: when you have an exhaustive `switch (err)` over the error union (enumerating every error variant), keep using `error.OutOfMemory => bun.outOfMemory()` inside the exhaustive switch arm. Do NOT recommend replacing `bun.outOfMemory()` with `bun.handleOom()` within those exhaustive switch arms. Only flag bare `catch bun.outOfMemory()` calls (i.e., non-exhaustive/non-switch handling), since `bun.handleOom()` is intended to wrap whole error-union expressions with `catch` to avoid accidentally swallowing non-OOM errors.

Learnt from: robobun
Repo: oven-sh/bun PR: 30147
File: src/bun.js/webcore/Body.zig:1777-1804
Timestamp: 2026-05-03T06:53:51.543Z
Learning: In this repo’s Zig code, avoid tail-calling (or otherwise delegating) between two mutually recursive functions when both rely on inferred error sets. Zig can fail with errors like “unable to resolve inferred error set” for the recursive cycle. If such mutually recursive structure is needed, apply the workaround used in Body.zig: inline the terminal-state handling directly (e.g., via an exhaustive switch) at the call site rather than tail-calling the other function that participates in the mutual recursion.

Learnt from: robobun
Repo: oven-sh/bun PR: 30219
File: src/resolver/package_json.zig:1731-1732
Timestamp: 2026-05-04T03:21:09.821Z
Learning: When reviewing this repo’s Zig code, avoid flagging repeated calls to `module_bufs.get()` (or other `bun.ThreadlocalBuffers(T).get()` instances) as redundant if they occur within the same function/scope. In `oven-sh/bun`’s `bun.ThreadlocalBuffers(T)` implementation, `get()` is `inline`, so after the first call the compiler typically reuses the same TLS value/register within that scope. For readability, it’s acceptable (and preferred) to call `get()` multiple times in the same scope rather than caching the value in a local variable and reusing it.

Learnt from: robobun
Repo: oven-sh/bun PR: 30314
File: src/cli/Arguments.zig:1046-1056
Timestamp: 2026-05-06T11:21:18.669Z
Learning: Boolean env var handling in Zig projects: In oven-sh/bun, env vars declared with kind.boolean (for example New(kind.boolean, \"NODE_USE_SYSTEM_CA\", .{ .default = false })) return a plain bool from .get(). The internal stringIsTruthy conversion is applied before returning the value, so callers should receive a bool, not an optional string. In code reviews, do not suggest capturing the result as ?[]const u8 (e.g., using |value|) or performing raw string comparisons like == \"0\" to infer truth for boolean-kind env vars. Rely on the .get() result and its boolean semantics. This guideline applies to files that read boolean env vars (e.g., src/cli/Arguments.zig) and related env_var usage (src/bun_core/env_var.zig).

Learnt from: robobun
Repo: oven-sh/bun PR: 30357
File: src/jsc/JSGlobalObject.zig:335-343
Timestamp: 2026-05-07T07:17:05.327Z
Learning: In this repo’s Zig code (e.g., Bun’s usage of `std.Io.Writer`), `std.Io.Writer.Error` is a single-variant set containing only `error{WriteFailed}`. Allocation/OOM failures are internally mapped to `error.WriteFailed` by `std.Io.Writer.Allocating.drain` (vendored `vendor/zig/lib/std/Io/Writer.zig`), so `error.OutOfMemory` is not catchable/distinguishable via `std.Io.Writer` error handling. Therefore, when reviewing `std.Io.Writer` operations, do not recommend splitting `catch` logic to handle `error.OutOfMemory` separately; a bare `catch` or `catch |err|` that only covers `error.WriteFailed` is correct.

Learnt from: ig-ant
Repo: oven-sh/bun PR: 30403
File: src/aio/MemoryPressureWatcher.zig:367-373
Timestamp: 2026-05-08T16:28:02.004Z
Learning: When reviewing Zig code, do not treat “accumulator-style” holdoff/elapsed-time loops as potentially wrong due to EINTR if the code measures elapsed time using repeated slices of `std.Thread.sleep` (e.g., `slept += N; std.Thread.sleep(N)`). On POSIX, `std.Thread.sleep` handles EINTR by restarting `nanosleep` with the remaining time (`req = rem; continue`), so each sleep slice consumes its full intended duration even if signals are delivered. In this case, an `slept += N` accumulator correctly tracks elapsed time without needing a deadline/`clock`-based approach. Only flag under-counting concerns if the code uses a sleep primitive that does NOT perform this EINTR retry behavior.

Learnt from: mchv
Repo: oven-sh/bun PR: 30625
File: src/test_runner/ScopeFunctions.zig:162-167
Timestamp: 2026-05-13T16:37:25.279Z
Learning: When calling `jsc.JSObject.createStructure(globalThis, owner, ...)` in Bun’s Zig bindings, ensure the `owner` argument is the JSValue that will actually hold references to the objects created from that structure (because it’s used only for the GC write barrier via `vm.writeBarrier(owner, structure)`). Do not pass a static context like `globalThis` or `vm` as the owner. Instead, pass a stable container such as `this.js_value` when callers cache the structure on a statement object, or pass a transient container like the result array JSValue when the structure is only needed for that array’s lifetime.

Learnt from: mchv
Repo: oven-sh/bun PR: 30625
File: src/test_runner/ScopeFunctions.zig:162-167
Timestamp: 2026-05-13T16:37:25.279Z
Learning: In this repo’s Zig code (oven-sh/bun), treat `jsc.JSObject.createStructure` as infallible: it internally performs GC-triggering heap allocations (`emptyObjectStructureForPrototype` + `addPropertyTransition`) but does not return a failure value (never returns null/invalid). During code review, do not flag the absence of validity/error checking on the returned `JSValue` from `createStructure` as a bug. Only apply this exemption to `jsc.JSObject.createStructure` (not other APIs).

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. bun crashed with error "panic(main thread): integer overflow oh no: Bun has crashed." when do bun test with @testing-library/react using toMatchSnapshot #19534 - Panic in pretty_format.zig with recursive forEach calls when using toMatchSnapshot with @testing-library/react — caused by .JSX missing from canHaveCircularReferences(), leading to infinite recursion on circular React element trees
  2. ToMatchSnapshot broken with react components #5540 - toMatchSnapshot with React components produces massive (20MB+) output — the non-crashing manifestation of the same bug: without circular reference detection for JSX elements, the formatter expands circular structures instead of truncating them

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #19534
Fixes #5540

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. console: guard JSX/Proxy recursion in Bun.inspect (stack overflow SIGSEGV) #29709 - Identical fix: adds .JSX to canHaveCircularReferences() and fixes unsafe props.getObject() unwrap in both ConsoleObject.zig and pretty_format.zig
  2. Fix stack overflow in Bun.inspect for circular JSX elements #30126 - Partial overlap: same canHaveCircularReferences() fix for JSX circular references, but omits the non-object props safety fix

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, well-scoped crash fix with regression tests.

Extended reasoning...

Overview

This PR fixes a fuzzer-discovered segfault in Bun.inspect() / console.log() and the jest pretty-formatter when handling malformed React elements. It touches three files: src/jsc/ConsoleObject.zig and src/test_runner/pretty_format.zig (mirrored fixes), plus four new regression tests in test/js/bun/util/inspect.test.js. The two code changes are (1) adding .JSX to canHaveCircularReferences() so the existing visited-set / stack-check guard applies to React elements, and (2) replacing the unsafe props.getObject().? unwrap with an if (props.getObject()) |props_obj| guard so primitive props values are skipped instead of panicking.

Security risks

None. This is purely defensive hardening of debug-output formatting code. The .JSX tag is only assigned when the value is already a JS object (js_type.isObject()), so adding it to the circular-reference set is safe and consistent with the other object-like tags already listed. The getObject() guard strictly narrows behavior (no new code path executes — it just skips the props block when props isn't an object) and removes a SAFETY: comment whose assumption was demonstrably false.

Level of scrutiny

Low. This is a small, mechanical crash fix in inspection/pretty-printing — non-semantic, output-only code that previously crashed on adversarial input. Both files receive the identical, idiomatic change (if |a| if |b| { ... };), and the surrounding control flow (defer, early return for children, trailing writer.writeAll(" />")) is preserved exactly. The canHaveCircularReferences mechanism is well-established for .Array/.Object/.Map/.Set and this just extends it to one more tag.

Other factors

Four targeted regression tests cover circular props, circular key, circular children, and five primitive props types. The PR description notes existing JSX inspect tests, console tests, and md-react.test.ts continue to pass. The bug-hunting system found no issues. No design decisions or behavioral trade-offs are involved — this is a straightforward Fuzzilli-found crash fix.

@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29709, which has the same fix with more comprehensive test coverage.

@robobun robobun closed this May 13, 2026
@robobun
robobun deleted the farm/301da87b/inspect-jsx-circular branch May 13, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant