diff --git a/src/signals/boundary-test-generation.ts b/src/signals/boundary-test-generation.ts index 9369b4dfd4..eb7ecbaa18 100644 --- a/src/signals/boundary-test-generation.ts +++ b/src/signals/boundary-test-generation.ts @@ -32,7 +32,11 @@ export type BoundaryTouch = { // true-positive set). Each pattern only matches an ADDED line (a line starting with a single `+`, not `++` // which is the `+++ b/file` patch header) so this only ever reacts to genuinely new code, never context lines // or the file the diff is against. -const ARRAY_INDEX_BOUNDS_PATTERN = /\[\s*(?:[\w.]+\.length|[\w.]+\.length\s*-\s*1|-1)\s*\]|\.length\s*(?:-\s*1)?\s*[<>]=?/; +// The `.at()` alternative catches the modern `arr.at(-1)` / `arr.at(0)` last-element/off-by-one +// idiom (bracket forms above miss it). Deliberately only a NUMERIC literal argument (optionally negative): +// a bare identifier like `.at(idx)` carries none of the specific boundary signal `-1`/`0` does, and matching +// it would reintroduce the false-positive noise this pattern set is kept small to avoid. +const ARRAY_INDEX_BOUNDS_PATTERN = /\[\s*(?:[\w.]+\.length|[\w.]+\.length\s*-\s*1|-1)\s*\]|\.length\s*(?:-\s*1)?\s*[<>]=?|\.at\(\s*-?\d+\s*\)/; const NULL_OR_UNDEFINED_BRANCH_PATTERN = /(?:===?|!==?)\s*(?:null|undefined)\b|\b(?:null|undefined)\s*(?:===?|!==?)|\?\?|\?\./; const EMPTY_COLLECTION_CHECK_PATTERN = /\.length\s*(?:===?|!==?|[<>]=?)\s*0\b|\blen\(.*\)\s*(?:===?|!==?|[<>]=?)\s*0\b|\.(?:isEmpty|is_empty)\s*\(/; diff --git a/test/unit/boundary-test-generation.test.ts b/test/unit/boundary-test-generation.test.ts index eda286d219..ad75d4e5dc 100644 --- a/test/unit/boundary-test-generation.test.ts +++ b/test/unit/boundary-test-generation.test.ts @@ -12,6 +12,19 @@ describe("detectBoundaryTouches", () => { expect(touches[0]).toMatchObject({ path: "src/list.ts", kind: "array_index_bounds" }); }); + it("detects the modern `.at()` last-element/off-by-one idiom as array/index bounds", () => { + for (const line of ["+const last = items.at(-1);\n", "+const head = items.at(0);\n", "+const penult = rows.at(-2);\n"]) { + const touches = detectBoundaryTouches([{ path: "src/list.ts", patch: line }]); + expect(touches).toHaveLength(1); + expect(touches[0]?.kind).toBe("array_index_bounds"); + } + }); + + it("does NOT flag `.at()` — a non-literal argument carries no off-by-one boundary signal", () => { + // Narrow-scope guard (#1972): only a numeric literal argument is a boundary tell; `.at(idx)` is not. + expect(detectBoundaryTouches([{ path: "src/list.ts", patch: "+const item = items.at(idx);\n" }])).toHaveLength(0); + }); + it("detects a null/undefined branch pattern in an added line", () => { const touches = detectBoundaryTouches([{ path: "src/user.ts", patch: "@@ -1,1 +1,2 @@\n+if (user === null) return defaultUser;\n" }]); expect(touches).toHaveLength(1);