Skip to content

Commit 34cc8f5

Browse files
committed
fix: map review line numbers from unified diff
1 parent 5248ede commit 34cc8f5

4 files changed

Lines changed: 147 additions & 17 deletions

File tree

__tests__/unit/analysis/finding-filter.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -830,19 +830,23 @@ describe('FindingFilter', () => {
830830

831831
test('keeps SQL interpolation regressions even when phrased cautiously', () => {
832832
const diff = `diff --git a/src/users.js b/src/users.js
833-
@@ -7,7 +7,7 @@ export function normalizeEmail(email) {
833+
index 51097d9..d0723db 100644
834+
--- a/src/users.js
835+
+++ b/src/users.js
836+
@@ -3,8 +3,7 @@ function normalizeEmail(email) {
834837
}
835838
836839
export async function findUserByEmail(db, email) {
837-
- const rows = await db.query('SELECT * FROM users WHERE email = ? LIMIT 1', [email]);
840+
- const normalized = normalizeEmail(email);
841+
- const rows = await db.query('SELECT * FROM users WHERE email = ? LIMIT 1', [normalized]);
838842
+ const rows = await db.query(\`SELECT * FROM users WHERE email = '\${email}' LIMIT 1\`);
839843
return rows[0] || null;
840844
}`;
841845

842846
const findings: Finding[] = [
843847
{
844848
file: 'src/users.js',
845-
line: 10,
849+
line: 6,
846850
severity: 'major',
847851
title: 'Email is interpolated into SQL',
848852
message:

dist/index.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23129,18 +23129,73 @@ var FindingFilter = class {
2312923129
if (!finding.line) {
2313023130
return false;
2313123131
}
23132-
const lines = diffContent.split("\n");
23133-
const lineIndex = finding.line - 1;
23134-
if (lineIndex < 0 || lineIndex >= lines.length) {
23132+
if (this.isTrueSecurityIssue(finding)) {
23133+
return false;
23134+
}
23135+
const patchLike = diffContent.includes("@@") || diffContent.includes("diff --git ");
23136+
const mappedLine = this.findNewFileLineInDiff(finding.file, finding.line, diffContent);
23137+
if (patchLike && mappedLine === null) {
2313523138
return false;
2313623139
}
23137-
const line = lines[lineIndex].trim();
23140+
if (!patchLike && mappedLine === null) {
23141+
const lines = diffContent.split("\n");
23142+
const lineIndex = finding.line - 1;
23143+
if (lineIndex < 0 || lineIndex >= lines.length) {
23144+
return false;
23145+
}
23146+
return this.isBlankBraceOrCommentLine(lines[lineIndex].trim(), finding.line);
23147+
}
23148+
if (mappedLine === null) {
23149+
return false;
23150+
}
23151+
return this.isBlankBraceOrCommentLine(mappedLine.trim(), finding.line);
23152+
}
23153+
isBlankBraceOrCommentLine(line, lineNumber) {
2313823154
if (line === "" || line === "}" || line === "};" || line === "])" || line === "]);" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
23139-
logger.debug(`Line ${finding.line} is blank/brace/comment, likely incorrect line number`);
23155+
logger.debug(`Line ${lineNumber} is blank/brace/comment, likely incorrect line number`);
2314023156
return true;
2314123157
}
2314223158
return false;
2314323159
}
23160+
findNewFileLineInDiff(file, lineNumber, diffContent) {
23161+
let currentFile = null;
23162+
let newLine = null;
23163+
for (const rawLine of diffContent.split("\n")) {
23164+
const fileMatch = rawLine.match(/^diff --git a\/.+ b\/(.+)$/);
23165+
if (fileMatch) {
23166+
currentFile = fileMatch[1];
23167+
newLine = null;
23168+
continue;
23169+
}
23170+
const newFileMatch = rawLine.match(/^\+\+\+ b\/(.+)$/);
23171+
if (newFileMatch) {
23172+
currentFile = newFileMatch[1];
23173+
continue;
23174+
}
23175+
const hunkMatch = rawLine.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
23176+
if (hunkMatch) {
23177+
newLine = Number(hunkMatch[1]);
23178+
continue;
23179+
}
23180+
if (newLine === null || currentFile !== file) {
23181+
continue;
23182+
}
23183+
if (rawLine.startsWith("---") || rawLine.startsWith("+++")) {
23184+
continue;
23185+
}
23186+
if (rawLine.startsWith("-")) {
23187+
continue;
23188+
}
23189+
if (rawLine.startsWith("+") || rawLine.startsWith(" ")) {
23190+
const lineText = rawLine.slice(1);
23191+
if (newLine === lineNumber) {
23192+
return lineText;
23193+
}
23194+
newLine++;
23195+
}
23196+
}
23197+
return null;
23198+
}
2314423199
/**
2314523200
* Check for invalid or suspicious line numbers that will cause GitHub API errors
2314623201
*/

dist/index.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/analysis/finding-filter.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -674,16 +674,39 @@ export class FindingFilter {
674674
return false; // No line number to check
675675
}
676676

677-
// Extract the line from the diff
678-
const lines = diffContent.split('\n');
679-
const lineIndex = finding.line - 1;
677+
// Do not suppress concrete security findings just because the model's line
678+
// maps near hunk context. GitHub validation later decides whether it is postable.
679+
if (this.isTrueSecurityIssue(finding)) {
680+
return false;
681+
}
680682

681-
if (lineIndex < 0 || lineIndex >= lines.length) {
682-
return false; // Line number out of bounds
683+
const patchLike = diffContent.includes('@@') || diffContent.includes('diff --git ');
684+
const mappedLine = this.findNewFileLineInDiff(finding.file, finding.line, diffContent);
685+
686+
if (patchLike && mappedLine === null) {
687+
return false; // Cannot prove the line is wrong from this patch.
683688
}
684689

685-
const line = lines[lineIndex].trim();
690+
if (!patchLike && mappedLine === null) {
691+
// Legacy fallback for tests/plain snippets that pass raw code instead of a patch.
692+
const lines = diffContent.split('\n');
693+
const lineIndex = finding.line - 1;
694+
695+
if (lineIndex < 0 || lineIndex >= lines.length) {
696+
return false; // Line number out of bounds
697+
}
698+
699+
return this.isBlankBraceOrCommentLine(lines[lineIndex].trim(), finding.line);
700+
}
686701

702+
if (mappedLine === null) {
703+
return false;
704+
}
705+
706+
return this.isBlankBraceOrCommentLine(mappedLine.trim(), finding.line);
707+
}
708+
709+
private isBlankBraceOrCommentLine(line: string, lineNumber: number): boolean {
687710
// Check if the line is just a closing brace, blank, or comment
688711
if (
689712
line === '' ||
@@ -695,13 +718,61 @@ export class FindingFilter {
695718
line.startsWith('/*') ||
696719
line.startsWith('*')
697720
) {
698-
logger.debug(`Line ${finding.line} is blank/brace/comment, likely incorrect line number`);
721+
logger.debug(`Line ${lineNumber} is blank/brace/comment, likely incorrect line number`);
699722
return true;
700723
}
701724

702725
return false;
703726
}
704727

728+
private findNewFileLineInDiff(file: string, lineNumber: number, diffContent: string): string | null {
729+
let currentFile: string | null = null;
730+
let newLine: number | null = null;
731+
732+
for (const rawLine of diffContent.split('\n')) {
733+
const fileMatch = rawLine.match(/^diff --git a\/.+ b\/(.+)$/);
734+
if (fileMatch) {
735+
currentFile = fileMatch[1];
736+
newLine = null;
737+
continue;
738+
}
739+
740+
const newFileMatch = rawLine.match(/^\+\+\+ b\/(.+)$/);
741+
if (newFileMatch) {
742+
currentFile = newFileMatch[1];
743+
continue;
744+
}
745+
746+
const hunkMatch = rawLine.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
747+
if (hunkMatch) {
748+
newLine = Number(hunkMatch[1]);
749+
continue;
750+
}
751+
752+
if (newLine === null || currentFile !== file) {
753+
continue;
754+
}
755+
756+
if (rawLine.startsWith('---') || rawLine.startsWith('+++')) {
757+
continue;
758+
}
759+
760+
if (rawLine.startsWith('-')) {
761+
continue;
762+
}
763+
764+
if (rawLine.startsWith('+') || rawLine.startsWith(' ')) {
765+
const lineText = rawLine.slice(1);
766+
if (newLine === lineNumber) {
767+
return lineText;
768+
}
769+
newLine++;
770+
}
771+
}
772+
773+
return null;
774+
}
775+
705776
/**
706777
* Check for invalid or suspicious line numbers that will cause GitHub API errors
707778
*/

0 commit comments

Comments
 (0)