Repository navigation
fix(utils): stop generateCodeFrame hanging on a CRLF source - #23345
NgoQuocViet2001 wants to merge 1 commit into
Conversation
The inner loop had no upper bound on j:
for (let j = i - range; j <= i + range || end > count; j++) {
if (j < 0 || j >= lines.length) continue
Once j runs past the last line the continue skips the count update at
the bottom of the body, so count freezes while end stays larger, and
the loop spins forever at 100% CPU.
It only shows on CRLF input. count advances by one per line break, but
end is clamped to source.length, which counts two characters per CRLF
-- so on a CRLF source end can exceed anything count reaches. On LF the
two always meet and the loop exits.
Bound j by lines.length in the second disjunct.
|
Is this also fixed by #23219? |
|
Yes — #23219 fixes it, and it's the better fix. I checked rather than guessed: I lifted The reason it works is that #23219 removes the cause rather than the symptom. The loop only fails to terminate because It's also more correct beyond termination. Taking "the frame should point to the same location regardless of the line endings" as the criterion (from #23219's own test) and comparing every Mine leaves the offset arithmetic wrong and only stops the spin; #23219 fixes So I'm happy to close this in favour of #23219 — say the word, or close it yourself if that's easier. One thing worth leaving on the record either way: even after #23219, the |
Description
generateCodeFrame()never returns — a 100% CPU spin, no allocation, no throw — when the source uses CRLF line endings and the highlighted range reaches the end of the file.The same calls on the LF equivalent return normally, which is why it has gone unnoticed: every existing test in the
generateCodeFramesblock that passes anenduses the LFsource.Why it loops
The inner loop has no upper bound on
j:Once
jpasses the last line, thecontinueskips thecountupdate at the bottom of the body, socountfreezes atsum(line lengths) + (lines.length - 1)whileendstays larger.end > countis then permanently true andjincrements forever.CRLF is what makes
endunreachable:countadvances by one per line break, butendis clamped tosource.length, which counts two characters per CRLF. So on a CRLF sourceendcan exceed anythingcountwill ever reach. On LF the two always meet and the loop exits.Instrumented, capped at 200k iterations:
The fix bounds
jbylines.lengthin the second disjunct, so the loop cannot outrun the input.This matters because the function runs on the error path —
formatError/buildErrorMessage— so a CRLF file (ordinary on Windows, and whatgit config core.autocrlf=truechecks out) turns a build or dev-server error into a hung process rather than a message.Validation
The regression test genuinely guards it: on
mainit hangs, so I ran it under an external timeout.Before the fix
After the fix
npx vitest run packages/vite/src/node/__tests__/utils.spec.ts— 123 passed (123).npx eslint packages/vite/src/node/utils.ts packages/vite/src/node/__tests__/utils.spec.ts— clean.npx oxfmt --checkon both — "All matched files use the correct format."pnpm --filter vite build— succeeds (includingbuild-types-check).Scope
This is the minimal fix: it makes the loop terminate. It does not change the frames that already worked, hence no snapshot churn.
There is a deeper issue in the same function that I deliberately left alone:
counthardcodes+ 1per separator, so on CRLF the underline lengths are computed against a position space two characters narrower thanend. Makingcountandendagree on the real separator width would fix that too, but it moves existing output and belongs in its own PR. Happy to follow up if you want it.