Skip to content

Optimizing performance for traversing stacks#3343

Merged
RobinTail merged 16 commits intomasterfrom
boost-stacks
Apr 24, 2026
Merged

Optimizing performance for traversing stacks#3343
RobinTail merged 16 commits intomasterfrom
boost-stacks

Conversation

@RobinTail
Copy link
Copy Markdown
Owner

@RobinTail RobinTail commented Apr 23, 2026

using index-based approach instead of Array::shift which has O(n) complexity.

That gives 2-20x boost in tests for (5-100 items)

 ✓ bench/queue-ops.bench.ts > 'small (5)' 11465ms
     name                         hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · shift() approach  13,363,719.01  0.0000  1.8336  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.72%   6681860
   · index approach    24,114,756.07  0.0000  0.9118  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.47%  12057379
   · pop() reverse     20,425,796.41  0.0000  0.0766  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.07%  10212899
   · forEach           24,495,518.63  0.0000  0.0377  0.0000  0.0000  0.0001  0.0001  0.0001  ±0.05%  12247760

 ✓ bench/queue-ops.bench.ts > 'medium (20)' 9208ms
     name                         hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · shift() approach   5,011,403.59  0.0001  0.2987  0.0002  0.0002  0.0003  0.0003  0.0005  ±0.13%   2505702
   · index approach    23,779,015.86  0.0000  0.4419  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.26%  11889508
   · pop() reverse     12,609,912.03  0.0000  1.9828  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.78%   6304957
   · forEach           23,855,668.43  0.0000  0.1539  0.0000  0.0000  0.0001  0.0001  0.0002  ±0.12%  11927835

 ✓ bench/queue-ops.bench.ts > 'large (100)' 7046ms
     name                         hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · shift() approach     994,668.73  0.0008  0.2203  0.0010  0.0010  0.0015  0.0016  0.0017  ±0.11%   497335
   · index approach    18,479,899.19  0.0000  0.0884  0.0001  0.0001  0.0001  0.0001  0.0003  ±0.07%  9239950
   · pop() reverse      4,205,916.53  0.0001  0.2371  0.0002  0.0003  0.0003  0.0003  0.0006  ±0.10%  2102959
   · forEach           18,812,352.95  0.0000  0.1113  0.0001  0.0001  0.0001  0.0001  0.0003  ±0.07%  9406177

Summary by CodeRabbit

  • Tests

    • Removed an outdated schema benchmark; added new queue-operation benchmarks across multiple sizes and iteration strategies; added routing traversal tests verifying depth-first visitation.
  • Refactor

    • Replaced front-of-queue mutation with index-based traversal across several traversals to preserve processing order; changed route handler assembly to append middleware incrementally.
  • Style

    • Added a lint rule flagging certain array mutation patterns and a targeted suppression for one occurrence.

@RobinTail RobinTail added the refactoring The better way to achieve the same result label Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2867d4aa-a3c5-4209-8960-d779469bf979

📥 Commits

Reviewing files that changed from the base of the PR and between 91133b1 and 243e0c8.

⛔ Files ignored due to path filters (1)
  • express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • express-zod-api/tests/routing-walker.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • express-zod-api/tests/routing-walker.spec.ts

📝 Walkthrough

Walkthrough

Replace front-removing queue traversal (shift/unshift) with index-based iteration across multiple schema, routing, and template utilities; remove a legacy can-merge benchmark and add a queue-ops benchmark exercising several queue-consumption strategies.

Changes

Cohort / File(s) Summary
Benchmarks
express-zod-api/bench/can-merge.bench.ts, express-zod-api/bench/queue-ops.bench.ts
Delete legacy can-merge.bench.ts; add queue-ops.bench.ts with Vitest parameterized benches comparing shift-loop, index-read loop, reverse+pop, and forEach across sizes 5, 20, 100.
Cycle detection & diagnostics
express-zod-api/src/deep-checks.ts, express-zod-api/src/diagnostics.ts
Change hasCycle and Diagnostics.#checkSchema from dequeue (shift()) loops to index-based for iteration while preserving push-based expansion and detection logic.
Reference fixing & schema flattening
express-zod-api/src/documentation-helpers.ts, express-zod-api/src/json-schema-helpers.ts
Switch fixReferences and flattenIO from shift()-based worklist processing to index-driven loops, keeping nested-value pushes and behavior unchanged.
Routing traversal & tests
express-zod-api/src/routing-walker.ts, express-zod-api/tests/routing-walker.spec.ts
Refactor walkRouting from shift/unshift to index iteration; insert children via splice(idx+1, 0, ...) to maintain traversal order; add tests asserting depth-first enumeration.
Template literal handling
express-zod-api/src/zts.ts
Refactor onTemplateLiteral to iterate def.parts by index, add readText() helper, and remove mutations via shift/unshift while preserving template emission flow.
Routing construction & integration linting
express-zod-api/src/routing.ts, express-zod-api/src/integration.ts
initRouting now builds handlers incrementally (append CORS, parsers, terminal handler) instead of unshift/concat; add inline ESLint suppression for one unshift site.
Lint rules
eslint.config.js
Add performanceConcerns via no-restricted-syntax to flag shift/unshift call sites with a performance warning message.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through stacks and left shift behind,
I counted by index and kept each node in mind.
Queues I probe—pop, reverse, read or forEach,
I splice new paths and let old mutations cease.
A rabbit cheers the cleaner kind.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: refactoring array traversal from shift-based (O(n)) to index-based iteration for performance gains across multiple modules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch boost-stacks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@RobinTail RobinTail changed the title fix(flattenIO): optimizing performance for walking stacks Optimizing performance for traversing stacks Apr 23, 2026
@coveralls-official
Copy link
Copy Markdown

coveralls-official Bot commented Apr 23, 2026

Coverage Status

coverage: 100.0%. remained the same — boost-stacks into master

@RobinTail RobinTail marked this pull request as ready for review April 23, 2026 20:31
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Reviewed — no issues found.

The shift() → index-based transformation is correct across all five call sites. Traversal order is preserved in every case, including routing-walker.ts where unshift()splice(idx, 0, ...) maintains depth-first semantics. The splice there still has linear cost per invocation, but it only runs when entering nested routing (not on every iteration like the old shift() did), so it's a net win.

Task list (4/4 completed)
  • Read the full diff and understand the changes
  • Read relevant source files for context
  • Analyze correctness, performance claims, and edge cases
  • Submit review

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@express-zod-api/bench/queue-ops.bench.ts`:
- Around line 28-32: The "index approach" benchmark is unfair because it never
reads elements; update the bench("index approach", ...) loop to dereference the
array element (e.g., access q[idx]) inside the while loop so it realistically
models traversal costs like in hasCycle, fixReferences, and walkRouting;
additionally, for parity capture the element returned by shift()/pop() in their
benches (store the result to a local variable) and move any reverse() call
outside the timed closure for the pop case so the timed section measures only
consumption cost.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac2a3fff-425f-4728-bc14-1fb992b89d35

📥 Commits

Reviewing files that changed from the base of the PR and between 4d77363 and c753a10.

📒 Files selected for processing (7)
  • express-zod-api/bench/can-merge.bench.ts
  • express-zod-api/bench/queue-ops.bench.ts
  • express-zod-api/src/deep-checks.ts
  • express-zod-api/src/diagnostics.ts
  • express-zod-api/src/documentation-helpers.ts
  • express-zod-api/src/json-schema-helpers.ts
  • express-zod-api/src/routing-walker.ts
💤 Files with no reviewable changes (1)
  • express-zod-api/bench/can-merge.bench.ts

Comment thread express-zod-api/bench/queue-ops.bench.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Extended the shift()/unshift() → index-based traversal optimization to onTemplateLiteral in zts.ts, where shiftText() and the outer span-building loop now share an idx cursor over the spread-copied parts array. The shared-state interplay between the two loops is correct — shiftText() always leaves idx at the next schema element or at the end, and the idx < parts.length guard for choosing createTemplateMiddle vs createTemplateTail remains equivalent to the original parts.length check.
  • Removed a redundant inline comment (// Handle potential undefined values) — the ?? "" is self-explanatory.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
express-zod-api/src/zts.ts (1)

43-70: Refactor looks correct; a couple of small cleanups possible.

Traced through the new index-based traversal for the expected alternating text, schema, text, … shape (and the degenerate single-text / back-to-back-schema cases) — head, span middles/tails, and the empty-spans fallback all match the previous shift-based behavior. Two optional tidy-ups:

  • const parts = [...def.parts] is no longer needed; with index iteration nothing is mutated, so def.parts can be used directly (or destructured) to drop the array clone.
  • shiftText no longer shifts anything — a name like consumeText/readText would better match the new mechanics.
♻️ Suggested cleanup
-  const parts = [...def.parts];
+  const { parts } = def;
   let idx = 0;
-  const shiftText = () => {
+  const readText = () => {
     let text = "";
     while (idx < parts.length) {
       const part = parts[idx++];
       if (isSchema(part)) {
         idx--;
         break;
       }
       text += part ?? "";
     }
     return text;
   };
-  const head = api.f.createTemplateHead(shiftText());
+  const head = api.f.createTemplateHead(readText());
   const spans: ts.TemplateLiteralTypeSpan[] = [];
   while (idx < parts.length) {
     const schema = next(parts[idx++] as z.core.$ZodType);
-    const text = shiftText();
+    const text = readText();
     const textWrapper =
       idx < parts.length
         ? api.f.createTemplateMiddle
         : api.f.createTemplateTail;
     spans.push(api.f.createTemplateLiteralTypeSpan(schema, textWrapper(text)));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/zts.ts` around lines 43 - 70, The clone const parts =
[...def.parts] is unnecessary since we no longer mutate the array; change usages
to read directly from def.parts (or destructure into a local const parts =
def.parts if you prefer) and remove the array copy, and rename the helper
shiftText to a clearer name like consumeText or readText to reflect that it does
not shift/mutate; update the references where shiftText() is called (used to
build head via api.f.createTemplateHead and when computing textWrapper/text)
accordingly so parts/idx logic remains the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@express-zod-api/src/zts.ts`:
- Around line 43-70: The clone const parts = [...def.parts] is unnecessary since
we no longer mutate the array; change usages to read directly from def.parts (or
destructure into a local const parts = def.parts if you prefer) and remove the
array copy, and rename the helper shiftText to a clearer name like consumeText
or readText to reflect that it does not shift/mutate; update the references
where shiftText() is called (used to build head via api.f.createTemplateHead and
when computing textWrapper/text) accordingly so parts/idx logic remains the
same.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcf123be-9c1a-4cb5-8cc7-79d4f941d734

📥 Commits

Reviewing files that changed from the base of the PR and between c753a10 and 000a1c2.

📒 Files selected for processing (1)
  • express-zod-api/src/zts.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
express-zod-api/src/zts.ts (1)

43-43: Optional: drop the now-redundant array copy.

Since parts is no longer mutated (no shift/unshift), the defensive spread copy is unnecessary and each call allocates a fresh array of length def.parts.length. Given this PR's perf focus, you can just alias the original.

♻️ Proposed tweak
-  const parts = [...def.parts];
+  const { parts } = def;
   let idx = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/zts.ts` at line 43, The local variable parts is making an
unnecessary defensive copy of def.parts; replace the spread copy by aliasing the
original array (use parts = def.parts) in the function that declares "const
parts = [...def.parts]" so that you avoid extra allocations — update the usage
sites in zts.ts to rely on the aliased parts variable instead of the copied
array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@express-zod-api/src/zts.ts`:
- Line 43: The local variable parts is making an unnecessary defensive copy of
def.parts; replace the spread copy by aliasing the original array (use parts =
def.parts) in the function that declares "const parts = [...def.parts]" so that
you avoid extra allocations — update the usage sites in zts.ts to rely on the
aliased parts variable instead of the copied array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 962d4886-5e69-47da-9bcd-7a19940f4ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 000a1c2 and 60834a6.

📒 Files selected for processing (1)
  • express-zod-api/src/zts.ts

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Removed the redundant [...def.parts] spread copy in onTemplateLiteral — the index-based traversal doesn't mutate the array, so destructuring def directly is correct and avoids an allocation per call.
  • Renamed shiftTextreadText to reflect the non-mutating index cursor semantics.
  • Fixed the "index approach" benchmark to actually dereference the array element (q[idx++]) instead of only incrementing the counter, making the comparison with shift()/pop()/forEach fair.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
express-zod-api/src/zts.ts (1)

45-56: Optional: peek instead of advance-then-rewind in readText.

The idx++ followed by idx-- when a schema is encountered is a slightly awkward advance-and-undo pattern. Peeking with parts[idx] before committing the increment reads more clearly and aligns with the straight idx++ pattern established in deep-checks.ts (context snippet 2). Behavior is identical.

♻️ Proposed refactor
-  const readText = () => {
-    let text = "";
-    while (idx < parts.length) {
-      const part = parts[idx++];
-      if (isSchema(part)) {
-        idx--;
-        break;
-      }
-      text += part ?? ""; // Handle potential undefined values
-    }
-    return text;
-  };
+  const readText = () => {
+    let text = "";
+    while (idx < parts.length && !isSchema(parts[idx])) {
+      text += parts[idx++] ?? ""; // Handle potential undefined values
+    }
+    return text;
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/zts.ts` around lines 45 - 56, The readText helper uses an
advance-then-rewind pattern (idx++ then idx-- when isSchema(part) is true);
change it to peek before advancing: check parts[idx] and call
isSchema(parts[idx]) first, break without modifying idx if it's a schema,
otherwise consume parts[idx] and increment idx after adding it to text. Update
references to parts, idx, and isSchema in readText so the logic mirrors the
straight idx++ consumption used elsewhere (e.g., deep-checks.ts) but avoids the
unnecessary decrement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@express-zod-api/src/zts.ts`:
- Around line 45-56: The readText helper uses an advance-then-rewind pattern
(idx++ then idx-- when isSchema(part) is true); change it to peek before
advancing: check parts[idx] and call isSchema(parts[idx]) first, break without
modifying idx if it's a schema, otherwise consume parts[idx] and increment idx
after adding it to text. Update references to parts, idx, and isSchema in
readText so the logic mirrors the straight idx++ consumption used elsewhere
(e.g., deep-checks.ts) but avoids the unnecessary decrement.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab094b4f-9516-4140-ac4d-e3789888f63b

📥 Commits

Reviewing files that changed from the base of the PR and between 60834a6 and fb85aa7.

📒 Files selected for processing (2)
  • express-zod-api/bench/queue-ops.bench.ts
  • express-zod-api/src/zts.ts
✅ Files skipped from review due to trivial changes (1)
  • express-zod-api/bench/queue-ops.bench.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
express-zod-api/src/routing-walker.ts (1)

94-116: LGTM — DFS visit order preserved via splice(idx + 1, 0, …).

Using splice(idx + 1, 0, ...children) instead of a simple push is the correct choice here: it maintains the depth-first visit order that the previous shift() + unshift(...children) produced, which (per collectSiblings in routing.ts) drives the insertion order into the familiar Map. Swapping this for push would flip to BFS and change endpoint registration order — worth a brief code comment if you want to harden against future refactors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/routing-walker.ts` around lines 94 - 116, The DFS order
is intentionally preserved by inserting children with splice(idx + 1, 0,
...processEntries(...)) instead of push; please add a brief explanatory comment
next to the stack.splice(...) call (inside the for loop in routing-walker.ts)
stating that using splice preserves DFS and the previous shift/unshift semantics
(which affect collectSiblings and the insertion order into the familiar Map),
and warn that switching to push would change traversal to BFS and alter endpoint
registration order (mentioning AbstractEndpoint and ServeStatic context to make
the intent clear).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@express-zod-api/src/routing-walker.ts`:
- Around line 94-116: The DFS order is intentionally preserved by inserting
children with splice(idx + 1, 0, ...processEntries(...)) instead of push; please
add a brief explanatory comment next to the stack.splice(...) call (inside the
for loop in routing-walker.ts) stating that using splice preserves DFS and the
previous shift/unshift semantics (which affect collectSiblings and the insertion
order into the familiar Map), and warn that switching to push would change
traversal to BFS and alter endpoint registration order (mentioning
AbstractEndpoint and ServeStatic context to make the intent clear).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c57a0070-aa8f-4cd3-b1e9-cf9b8089f5d8

📥 Commits

Reviewing files that changed from the base of the PR and between fb85aa7 and bb95ef4.

📒 Files selected for processing (7)
  • express-zod-api/bench/queue-ops.bench.ts
  • express-zod-api/src/deep-checks.ts
  • express-zod-api/src/diagnostics.ts
  • express-zod-api/src/documentation-helpers.ts
  • express-zod-api/src/json-schema-helpers.ts
  • express-zod-api/src/routing-walker.ts
  • express-zod-api/src/zts.ts
✅ Files skipped from review due to trivial changes (3)
  • express-zod-api/src/deep-checks.ts
  • express-zod-api/src/diagnostics.ts
  • express-zod-api/bench/queue-ops.bench.ts

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Added a no-restricted-syntax lint rule banning shift(), unshift(), and splice() calls in performanceConcerns, enforcing index-based iteration as the default going forward — call sites that legitimately need these operations (integration.ts, routing-walker.ts, json-schema-helpers.ts) carry eslint-disable comments with justifications.
  • Refactored handler construction in initRouting to build the handlers array sequentially via push() — CORS first (conditionally), then parsers and endpoint handler — eliminating the prior slice().concat() + unshift() pattern and removing the defensive copy of matchingParsers.
  • Simplified readText in onTemplateLiteral to peek at parts[idx] before advancing, removing the advance-then-rewind (idx++ / idx--) pattern from the previous iteration.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
eslint.config.js (1)

75-78: Rule LGTM; minor note on splice wording.

The selector correctly targets the three mutators and the warn level plus per-line escape hatch (already used in routing-walker.ts) gives an acceptable ergonomic path. One nit: the message "shifting is 2-20x slower than index-based iteration" is accurate for shift/unshift, but splice is a broader API — the in-place insertion use in routing-walker.ts isn't really "shifting". If you want the rule to read precisely where it fires, consider either splitting splice into its own entry with a tailored message (e.g., "prefer index-based iteration over array mutation") or rephrasing the shared message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.js` around lines 75 - 78, The rule message lumps shift/unshift
and splice together but only shift/unshift are accurately described as "shifting
is 2-20x slower than index-based iteration"; update the ESLint config by either
(A) splitting the selector
"CallExpression[callee.property.name=/^(shift|unshift|splice)$/]" into two
entries—one targeting shift|unshift with the existing message and a separate
selector for splice with a tailored message like "prefer index-based iteration
or non-mutating methods over array mutation"—or (B) rephrase the shared message
to cover all three (e.g., "avoid array mutation; prefer index-based iteration or
non-mutating methods") so the message accurately reflects why the rule fires;
reference the existing selector string and message field when making the change.
express-zod-api/src/routing.ts (1)

92-109: Handler ordering preserved — LGTM.

The new push-based assembly yields [cors?, ...parsers, exec], matching the previous unshift-based result and preserving the issue #2706 invariant that CORS runs before parsers. Closures correctly capture accessMethods, endpoint, and matchingParsers per-method iteration.

Optional nit: the trailing colon in the inline comment on line 92 (// issue #2706: CORS must go before parsers:) reads like a dangling sentence since the related if (config.cors) is on the next line. Consider promoting it to a standalone comment directly above the if block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/routing.ts` around lines 92 - 109, Move the inline
comment about issue `#2706` so it's a standalone comment immediately above the if
(config.cors) block and remove the trailing colon to read as a complete
sentence; this keeps the note visible and avoids the dangling-colon style while
preserving behavior of handlers, config.cors, matchingParsers, and the final
app[method](path, ...handlers) assembly.
express-zod-api/src/routing-walker.ts (1)

94-95: Traversal semantics preserved — LGTM.

Verified DFS equivalence:

  • Old: shift() + unshift(children) ⇒ next iteration processes first child.
  • New: stack[idx] + splice(idx + 1, 0, ...children)idx++ lands on the first child.

So onEndpoint invocation order, duplicate detection via visited, and downstream familiar Map population in collectSiblings are unchanged. Performance win comes from O(1) index access on leaf iterations; splice is still O(n) but only pays that cost on container expansion.

Two small notes:

  1. The stack is never pruned, so peak memory is now proportional to the total number of routing entries rather than to DFS depth. Practically irrelevant for typical routings, but worth being aware of.
  2. The eslint-disable reason on line 113 ("acceptable due to conditional logic") is a bit opaque — something like "splice used for in-place insertion, not front-shifting" would make the intent self-evident next time someone grep's the suppressions.

Also applies to: 113-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/src/routing-walker.ts` around lines 94 - 95, Update the
opaque eslint-disable comment near the splice usage to a clear rationale like
"splice used for in-place insertion to preserve DFS traversal order; allowed
instead of shifting the front" so future greps show intent (referencing stack
and splice in routing-walker.ts); additionally, to address the note about peak
memory, prune already-processed entries after advancing (e.g., trim or compact
stack after handling a container) to keep peak size closer to DFS depth while
preserving onEndpoint invocation order, visited duplicate detection, and
collectSiblings/familiar Map semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@eslint.config.js`:
- Around line 75-78: The rule message lumps shift/unshift and splice together
but only shift/unshift are accurately described as "shifting is 2-20x slower
than index-based iteration"; update the ESLint config by either (A) splitting
the selector "CallExpression[callee.property.name=/^(shift|unshift|splice)$/]"
into two entries—one targeting shift|unshift with the existing message and a
separate selector for splice with a tailored message like "prefer index-based
iteration or non-mutating methods over array mutation"—or (B) rephrase the
shared message to cover all three (e.g., "avoid array mutation; prefer
index-based iteration or non-mutating methods") so the message accurately
reflects why the rule fires; reference the existing selector string and message
field when making the change.

In `@express-zod-api/src/routing-walker.ts`:
- Around line 94-95: Update the opaque eslint-disable comment near the splice
usage to a clear rationale like "splice used for in-place insertion to preserve
DFS traversal order; allowed instead of shifting the front" so future greps show
intent (referencing stack and splice in routing-walker.ts); additionally, to
address the note about peak memory, prune already-processed entries after
advancing (e.g., trim or compact stack after handling a container) to keep peak
size closer to DFS depth while preserving onEndpoint invocation order, visited
duplicate detection, and collectSiblings/familiar Map semantics.

In `@express-zod-api/src/routing.ts`:
- Around line 92-109: Move the inline comment about issue `#2706` so it's a
standalone comment immediately above the if (config.cors) block and remove the
trailing colon to read as a complete sentence; this keeps the note visible and
avoids the dangling-colon style while preserving behavior of handlers,
config.cors, matchingParsers, and the final app[method](path, ...handlers)
assembly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c032c088-b21b-4556-a018-b5a5540819a4

📥 Commits

Reviewing files that changed from the base of the PR and between bb95ef4 and 0c846ad.

📒 Files selected for processing (4)
  • eslint.config.js
  • express-zod-api/src/integration.ts
  • express-zod-api/src/routing-walker.ts
  • express-zod-api/src/routing.ts
✅ Files skipped from review due to trivial changes (1)
  • express-zod-api/src/integration.ts

@RobinTail RobinTail added the coverage Additional tests label Apr 24, 2026
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

Minor convention inconsistency in the new test file.

  • Narrowed the no-restricted-syntax lint rule to only ban shift/unshift, dropping splice — the "shifting" message no longer applies inaccurately to splice call sites.
  • Removed the now-unnecessary eslint-disable comment from routing-walker.ts.
  • Added routing-walker.spec.ts with two tests asserting DFS traversal order of walkRouting(), protecting the splice(idx + 1, 0, ...) invariant against accidental regression to BFS.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread express-zod-api/tests/routing-walker.spec.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
express-zod-api/tests/routing-walker.spec.ts (1)

5-48: Good targeted coverage for the new index-based traversal.

The two cases effectively pin the depth-first / nested-before-siblings invariant that the stack.splice(idx + 1, 0, ...) insertion in routing-walker.ts relies on, which protects the refactor against accidental regressions (e.g., switching back to append-at-end).

One optional nit: the two tests share identical setup (onEndpoint = vi.fn(), same config) — they could be consolidated via test.each() (routing + expected calls) per the repo's parameterized-test guideline, but it's fine as-is for just two cases.

As per coding guidelines: "Test files should use test.each() for parameterized tests with placeholders %s for current value and %# for index".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@express-zod-api/tests/routing-walker.spec.ts` around lines 5 - 48, Replace
the two nearly-identical tests with a single parameterized test using test.each
to follow the repository guideline: create an array of cases pairing a short
name, the routing object, and the expected onEndpoint.mock.calls, then call
test.each(cases)("%s %#", (name, routing, expected) => { const onEndpoint =
vi.fn(); walkRouting({ routing, config: { cors: false, methodLikeRouteBehavior:
"path" }, onEndpoint }); expect(onEndpoint.mock.calls).toEqual(expected); });
Keep references to walkRouting, onEndpoint, and the config object so the same
assertions (e.g., ["get","/v1/user/retrieve", endpoint]) are used as expected
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@express-zod-api/tests/routing-walker.spec.ts`:
- Around line 5-48: Replace the two nearly-identical tests with a single
parameterized test using test.each to follow the repository guideline: create an
array of cases pairing a short name, the routing object, and the expected
onEndpoint.mock.calls, then call test.each(cases)("%s %#", (name, routing,
expected) => { const onEndpoint = vi.fn(); walkRouting({ routing, config: {
cors: false, methodLikeRouteBehavior: "path" }, onEndpoint });
expect(onEndpoint.mock.calls).toEqual(expected); }); Keep references to
walkRouting, onEndpoint, and the config object so the same assertions (e.g.,
["get","/v1/user/retrieve", endpoint]) are used as expected values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a07b74a5-18d9-401b-83c4-ccb9f88989b1

📥 Commits

Reviewing files that changed from the base of the PR and between 0c846ad and 6e44628.

📒 Files selected for processing (3)
  • eslint.config.js
  • express-zod-api/src/routing-walker.ts
  • express-zod-api/tests/routing-walker.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • eslint.config.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • express-zod-api/src/routing-walker.ts

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented Apr 24, 2026

No action needed — the PR author already fixed the vitest import in routing-walker.spec.ts and confirmed with "fixed." Resolved the thread.

Task list (3/3 completed)
  • Review the feedback thread on routing-walker.spec.ts
  • Check if the change was already made by the PR author
  • Resolve thread and report progress

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Removed the vitest import from routing-walker.spec.ts, relying on vitest globals per project convention.
  • Dropped the redundant methodLikeRouteBehavior: "path" config option from both tests (defaults to "method", irrelevant for these fixtures) and renamed the post route fixture to record to avoid ambiguity with the HTTP method name.

Prior review feedback:

  • Remove vitest import from routing-walker.spec.ts — addressed

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Consolidated the two separate DFS-order tests in routing-walker.spec.ts into a single test.each<Routing> parameterized test with toMatchSnapshot() assertions, following the repo's testing convention. The hoisted onEndpoint mock with afterEach cleanup correctly handles shared state across iterations.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Owner Author

@RobinTail RobinTail left a comment

Choose a reason for hiding this comment

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

🏁

@RobinTail RobinTail merged commit 6898627 into master Apr 24, 2026
14 checks passed
@RobinTail RobinTail deleted the boost-stacks branch April 24, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coverage Additional tests refactoring The better way to achieve the same result

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant