mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(lint): match moved code by ordered near-match, not strict contiguity (#16385)
A diagnostic's span often reaches past the block a split moved — most commonly to a hook dependency array, which legitimately grows when closure variables become props. Requiring every line of the span to match contiguously reported the moved body as new. The block must still start at the same line in the base and appear in order, and >=90% of it must be present. Genuinely new code shares neither the anchor nor the ordering.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user