diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs index bc661dc67b7..5871557607b 100644 --- a/config/scripts/check-changed-code-quality.mjs +++ b/config/scripts/check-changed-code-quality.mjs @@ -178,17 +178,32 @@ export function isMovedCode(highlightedLines, baseBlocks) { if (needle.length === 0) { return false } + // Why a near-match rather than an exact contiguous one: a split moves a block + // verbatim but a diagnostic's span often reaches past it — most commonly to a + // hook dependency array, which legitimately grows when closure variables become + // props. Requiring every line to match would report the moved body as new. So: + // the block must still start at the same line in the base and appear IN ORDER, + // and nearly all of it must be present. Genuinely new code shares neither the + // anchor nor the ordering, so it stays reported. + const MIN_COVERAGE = 0.9 return baseBlocks.some((rawHaystack) => { const haystack = rawHaystack.map(normalizeSourceLine).filter((line) => line !== '') - for (let start = 0; start + needle.length <= haystack.length; start += 1) { - let matched = true - for (let offset = 0; offset < needle.length; offset += 1) { - if (haystack[start + offset] !== needle[offset]) { - matched = false - break + for (let start = 0; start < haystack.length; start += 1) { + if (haystack[start] !== needle[0]) { + continue + } + let matched = 1 + let cursor = start + 1 + for (let index = 1; index < needle.length && cursor < haystack.length; index += 1) { + while (cursor < haystack.length && haystack[cursor] !== needle[index]) { + cursor += 1 + } + if (cursor < haystack.length) { + matched += 1 + cursor += 1 } } - if (matched) { + if (matched / needle.length >= MIN_COVERAGE) { return true } } diff --git a/config/scripts/check-changed-code-quality.test.mjs b/config/scripts/check-changed-code-quality.test.mjs index 73cc12a78c6..76a25802e5c 100644 --- a/config/scripts/check-changed-code-quality.test.mjs +++ b/config/scripts/check-changed-code-quality.test.mjs @@ -75,9 +75,24 @@ describe('moved-code exemption', () => { expect(isMovedCode(['doThing()', 'newlyAddedSideEffect()'], base)).toBe(false) }) - it('does not exempt when the base lines are non-contiguous', () => { + it('tolerates a few lines appended inside the moved block', () => { + // A split commonly grows a hook dependency array when closure variables + // become props; the moved body around it is still moved. + const body = Array.from({ length: 20 }, (_, i) => `line${i}()`) + const base = [body] + const moved = [...body.slice(0, 19), 'newDep,', body[19]] + expect(isMovedCode(moved, base)).toBe(true) + }) + + it('does not exempt when the anchor line is absent from the base', () => { const base = [['doThing()', 'filler()', 'other()']] - expect(isMovedCode(['doThing()', 'other()'], base)).toBe(false) + expect(isMovedCode(['brandNewCall()', 'doThing()', 'other()'], base)).toBe(false) + }) + + it('does not exempt when most of the block is absent from the base', () => { + const base = [['keep0()', 'keep1()', 'unrelated()']] + const mostlyNew = ['keep0()', ...Array.from({ length: 18 }, (_, i) => `fresh${i}()`)] + expect(isMovedCode(mostlyNew, base)).toBe(false) }) it('ignores blank lines when matching', () => {