Skip to content

Commit 49aa2eb

Browse files
committed
fix(review-enrichment): include changed paths in dead-export caller checks
1 parent e451ceb commit 49aa2eb

2 files changed

Lines changed: 60 additions & 6 deletions

File tree

review-enrichment/src/analyzers/caller-impact.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ function symbolBoundaryRegex(symbol: string): RegExp {
7373
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7474
return new RegExp(
7575
`(?:^|[^A-Za-z0-9_$])(${escaped})(?:$|[^A-Za-z0-9_$])`,
76-
"g",
7776
);
7877
}
7978

@@ -265,6 +264,7 @@ async function readFileContainsSymbol(
265264
headSha: string,
266265
token: string,
267266
fetchImpl: typeof fetch,
267+
skipLineNumbers: ReadonlySet<number> = new Set(),
268268
options: ScanOptions = {},
269269
): Promise<boolean> {
270270
const encodedPath = encodePath(path);
@@ -297,7 +297,15 @@ async function readFileContainsSymbol(
297297
});
298298
if (!response.ok) return false;
299299
const text = response.data;
300-
return symbolBoundaryRegex(symbol).test(text);
300+
const lineRegex = symbolBoundaryRegex(symbol);
301+
for (const [index, line] of text.split("\n").entries()) {
302+
const lineNumber = index + 1;
303+
if (skipLineNumbers.has(lineNumber)) continue;
304+
if (lineRegex.test(line)) {
305+
return true;
306+
}
307+
}
308+
return false;
301309
} catch {
302310
return false;
303311
}
@@ -310,6 +318,8 @@ async function resolveCallers(
310318
headSha: string,
311319
token: string,
312320
skipPaths: Set<string> | null,
321+
additionalPaths: Iterable<string> = [],
322+
skipLineNumbersByPath: ReadonlyMap<string, ReadonlySet<number>> = new Map(),
313323
fetchImpl: typeof fetch,
314324
options: ScanOptions = {},
315325
): Promise<string[]> {
@@ -321,8 +331,10 @@ async function resolveCallers(
321331
fetchImpl,
322332
options,
323333
);
334+
const candidatePaths = new Set(hitPaths);
335+
for (const path of additionalPaths) candidatePaths.add(path);
324336
const callers: string[] = [];
325-
for (const path of hitPaths) {
337+
for (const path of candidatePaths) {
326338
if (skipPaths?.has(path)) continue;
327339
if (callers.length >= MAX_CALLERS_PER_SYMBOL) break;
328340
if (
@@ -334,6 +346,7 @@ async function resolveCallers(
334346
headSha,
335347
token,
336348
fetchImpl,
349+
skipLineNumbersByPath.get(path) ?? new Set(),
337350
options,
338351
)
339352
) {
@@ -471,13 +484,24 @@ export async function scanCallerImpact(
471484
const findings: CallerImpactFinding[] = [];
472485
for (const candidate of candidates) {
473486
if (options.signal?.aborted) throw new Error("analyzer_aborted");
487+
const skipLineNumbersByPath =
488+
candidate.kind === "dead"
489+
? new Map([[candidate.file, new Set([candidate.line])]])
490+
: new Map<string, Set<number>>();
491+
const isDead = candidate.kind === "dead";
492+
const skipPaths = isDead ? new Set([candidate.file]) : changedPaths;
493+
const additionalPaths = isDead
494+
? Array.from(changedPaths).filter((path) => path !== candidate.file)
495+
: [];
474496
const callers = await resolveCallers(
475497
repo.owner,
476498
repo.repo,
477499
candidate.searchSymbol,
478500
req.headSha,
479501
req.githubToken,
480-
candidate.kind === "dead" ? null : changedPaths,
502+
skipPaths,
503+
additionalPaths,
504+
skipLineNumbersByPath,
481505
fetchImpl,
482506
options,
483507
);

review-enrichment/test/caller-impact.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,34 @@ test("scanCallerImpact: dead new export is reported without unchanged callers",
205205
assert.deepEqual(findings[0].callers, []);
206206
});
207207

208-
test("scanCallerImpact: does not flag dead when only changed files call new export", async () => {
208+
test("scanCallerImpact: import-only references are still live callers", async () => {
209+
const findings = await scanCallerImpact(
210+
req([
211+
{
212+
path: "src/api.ts",
213+
patch:
214+
"@@ -1,1 +1,1 @@\n-export function doThing(a: number) {}\n+export function doThing(a: string) {}",
215+
},
216+
]),
217+
fetchFor(
218+
expectSearch([{ path: "src/consumer.ts" }]),
219+
fileContentsRouter([
220+
["/contents/src/api.ts?ref=abc123", "export function doThing(a: string) {}"],
221+
[
222+
"/contents/src/consumer.ts?ref=abc123",
223+
"import { doThing } from './api';",
224+
],
225+
]),
226+
),
227+
);
228+
229+
assert.equal(findings.length, 1);
230+
assert.equal(findings[0].kind, "changed");
231+
assert.equal(findings[0].symbol, "doThing");
232+
assert.deepEqual(findings[0].callers, ["src/consumer.ts"]);
233+
});
234+
235+
test("scanCallerImpact: dead detection ignores changed-file callsites", async () => {
209236
const findings = await scanCallerImpact(
210237
req([
211238
{
@@ -222,7 +249,10 @@ test("scanCallerImpact: does not flag dead when only changed files call new expo
222249
),
223250
);
224251

225-
assert.equal(findings.length, 0);
252+
assert.equal(findings.length, 1);
253+
assert.equal(findings[0].kind, "dead");
254+
assert.equal(findings[0].symbol, "localEntry");
255+
assert.deepEqual(findings[0].callers, []);
226256
});
227257

228258
test("scanCallerImpact: missing token/head-sha skips analysis", async () => {

0 commit comments

Comments
 (0)