Skip to content

bug: render-demo wrap() infinite-loops on a long path with no spaces #81

Description

@aryamthecodebreaker

Summary

wrap() in scripts/render-demo.mjs loops forever when a report line longer than WRAP_COLUMNS contains no space in its wrappable window. String.prototype.lastIndexOf returns -1, and every use of that -1 happens to be silently valid:

function wrap(line) {
  if (line.length <= WRAP_COLUMNS) return [line];
  const rows = [];
  let rest = line;
  let indent = "";
  while (rest.length > WRAP_COLUMNS - indent.length) {
    const slice = rest.slice(0, WRAP_COLUMNS - indent.length);
    const breakAt = slice.lastIndexOf(" ");        // -1 when there is no space
    rows.push(indent + rest.slice(0, breakAt).trimEnd());  // slice(0, -1) drops the LAST char
    rest = rest.slice(breakAt + 1);                        // slice(0) — rest is UNCHANGED
    indent = "    ";
  }
  rows.push(indent + rest);
  return rows;
}

rest.slice(breakAt + 1) becomes rest.slice(0), so rest never shrinks, the loop condition never becomes false, and rows grows without bound until the process exhausts memory.

Reproduction

A single context or changed file whose path is ~88+ characters is enough. renderMarkdownReport emits changed files as - `<path>` , so the leading "- " is consumed on the first iteration and the bare path — which contains no spaces — is what spins:

const WRAP_COLUMNS = 92;
function wrap(line) { /* as above, with an iteration guard */ }

const longPath = "packages/core/src/components/dashboard/widgets/analytics/RevenueBreakdownByRegion.test.tsx"; // 90 chars
wrap("- `" + longPath + "`");
path length: 90
NO PROGRESS after 20 iterations. rest.length still 92

Without the guard, node scripts/render-demo.mjs hangs indefinitely.

Impact

This is the documented way to regenerate the README hero image:

<!-- Reproducible recording: regenerate with `npm run build:cli && node scripts/render-demo.mjs` -->

The script currently works only because examples/tiny-auth-app has short paths. Pointing it at any realistically nested repository — or simply adding one deeply nested file to the example — turns the documented command into a hang with no error message. It is not covered by npm run ci, so nothing would catch the regression.

Suggested fix

Handle the no-space case by hard-breaking at the column limit:

const width = WRAP_COLUMNS - indent.length;
const slice = rest.slice(0, width);
const breakAt = slice.lastIndexOf(" ");
if (breakAt <= 0) {                 // no space, or a leading space: hard break
  rows.push(indent + slice);
  rest = rest.slice(width);
} else {
  rows.push(indent + rest.slice(0, breakAt).trimEnd());
  rest = rest.slice(breakAt + 1);
}
indent = "    ";

breakAt <= 0 also covers the breakAt === 0 case, which currently pushes an empty row for every line that begins with a space.

Worth adding a unit test for wrap() with a long unbroken token — it is a pure function, so it costs one assertion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueApproachable contribution with a clear scope

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions