refactor: prevent array index out of bounds - #56
Conversation
This PR refactors the loop conditions to ensure safe traversal of the source array and eliminate potential out-of-bounds access. - Array index possibly out of bounds: The original code allowed the loop index to reach `src.length` (using `i <= src.length`), causing an attempt to access `src[src.length]` which is undefined. We updated the loop condition to `i < src.length` and adjusted the break check to `i === src.length - 1` to confine iteration strictly within valid array indices. > This Autofix was generated by AI. Please review the change before merging.
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| Docker | Aug 7, 2026 4:16a.m. | Review ↗ | |
| Python | Aug 7, 2026 4:16a.m. | Review ↗ | |
| JavaScript | Aug 7, 2026 4:16a.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
There was a problem hiding this comment.
Caution
This PR breaks the default 2D highway for any chart containing chords. Moving the boundary break to i === src.length - 1 means the final chain's last element is never written to _chordRenderInfo, and pass 2 of _ensureChordRenderCache dereferences that entry unconditionally — so every chart with at least one chord throws a TypeError and chord rendering (plus everything drawn after chords) breaks.
Reviewed changes
_ensureChordRenderCachechain pass (static/js/highway-draw.js) — replaced thei <= src.lengthsentinel loop withi < src.lengthplus a boundary break ati === src.length - 1, in response to a DeepSource "array index possibly out of bounds" warning.
Big Pickle (free) | 𝕏
| for (let i = 0; i < src.length; i++) { | ||
| const breakHere = (i === src.length - 1) || |
There was a problem hiding this comment.
This breaks the default 2D highway for every chart that has chords. The break now fires at the last index, but the flush body covers [chainStart, i-1], so the final chord never gets a _chordRenderInfo entry — and pass 2 (line 1199) dereferences info unconditionally, throwing TypeError: Cannot set properties of undefined (setting 'baseFret').
I verified this against the real module: every non-empty chord array (single chord, two/three same-chain, final-chain-of-one) crashes; the original i <= src.length sentinel flushed [chainStart, src.length-1] and worked. The DeepSource warning was a false positive — at i === src.length the i === src.length clause short-circuits before src[i] is ever read.
Technical details
# Preserve the trailing-chain flush without an OOB-looking sentinel
## Affected sites
- static/js/highway-draw.js:1169-1192 — pass 1 never writes `_chordRenderInfo` for the last element of the array
- static/js/highway-draw.js:1199-1227 — pass 2 reads `info` unconditionally (line 1209 `info.baseFret = minF`) → TypeError on any missing entry
- static/highway.js:688 — `drawChords` runs inside the default renderer's try/catch (line 705), so the crash logs `draw error:` and skips `drawFretNumbers`, plugin draw hooks, and lyrics every frame the last chord is visible
## Required outcome
- Every chord gets a render-cache entry; the final chain is flushed including its last element; and `src[i]` accesses stay strictly in-bounds so the static analyzer stays satisfied.
## Suggested approach
Decide the break from the NEXT element instead of sentineling at the boundary, so the flush covers `[chainStart, i]` inclusive:
```js
for (let i = 0; i < src.length; i++) {
const next = src[i + 1];
const breakHere = next === undefined ||
(next.id !== src[i].id || Math.abs(next.t - src[i].t) >= CHAIN_GAP_THRESHOLD);
if (breakHere) {
const len = i - chainStart + 1;
for (let k = chainStart; k <= i; k++) {
/* existing per-chord entry construction */
}
chainStart = i + 1;
}
}
```
This is behavior-identical to the original: a gap/id-change between `i` and `i+1` flushes `[chainStart, i]` exactly as the old sentinel did, and the final element is flushed when `next === undefined`. The `i > chainStart` guard can be dropped — the flush is always non-empty. (`src[i + 1]` at the last index is a safe `undefined` comparison, so no analyzer false positive.)
This PR refactors the loop conditions to ensure safe traversal of the source array and eliminate potential out-of-bounds access.
src.length(usingi <= src.length), causing an attempt to accesssrc[src.length]which is undefined. We updated the loop condition toi < src.lengthand adjusted the break check toi === src.length - 1to confine iteration strictly within valid array indices.