Skip to content

Commit 700d4be

Browse files
committed
fix: address unresolved PR review comments
- Fix deletion-only hunk misclassification in line-level granularity by adding hunksParsed flag to DiffFileEntry; parseGitDiffOutput sets it when @@ headers are seen, and diffSarifByCommits uses it to distinguish "no hunk info" from "deletion-only" diffs - Precompute normalized diff paths once before the results loop, removing the unused diffPathMatchesSarifUri wrapper - Migrate all params_test.go from t.TempDir() to project-local .tmp/ - Add regression tests for deletion-only diffs in unit and handler tests
1 parent cbe5f6a commit 700d4be

File tree

7 files changed

+112
-40
lines changed

7 files changed

+112
-40
lines changed

client/internal/testing/params_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func projectTmpDir(t *testing.T, name string) string {
3939

4040
func TestBuildToolParams_TestConfig(t *testing.T) {
4141
// Create a temp test fixture with test-config.json
42-
dir := t.TempDir()
42+
dir := projectTmpDir(t, "test-config")
4343
testDir := filepath.Join(dir, "tools", "my_tool", "my_test")
4444
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
4545
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -56,7 +56,7 @@ func TestBuildToolParams_TestConfig(t *testing.T) {
5656
}
5757

5858
func TestBuildToolParams_MonitoringStateParams(t *testing.T) {
59-
dir := t.TempDir()
59+
dir := projectTmpDir(t, "monitoring-state")
6060
testDir := filepath.Join(dir, "tools", "codeql_lsp_completion", "basic")
6161
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
6262
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -77,7 +77,7 @@ func TestBuildToolParams_MonitoringStateParams(t *testing.T) {
7777
}
7878

7979
func TestBuildToolParams_ValidateCodeqlQuery(t *testing.T) {
80-
dir := t.TempDir()
80+
dir := projectTmpDir(t, "validate-query")
8181
testDir := filepath.Join(dir, "tools", "validate_codeql_query", "syntax_validation")
8282
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
8383
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -97,7 +97,7 @@ func TestBuildToolParams_ValidateCodeqlQuery(t *testing.T) {
9797
}
9898

9999
func TestBuildToolParams_ResolveQueries_UsesDirectoryKey(t *testing.T) {
100-
dir := t.TempDir()
100+
dir := projectTmpDir(t, "resolve-queries")
101101
testDir := filepath.Join(dir, "tools", "codeql_resolve_queries", "resolve_queries")
102102
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
103103
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -116,7 +116,7 @@ func TestBuildToolParams_ResolveQueries_UsesDirectoryKey(t *testing.T) {
116116
}
117117

118118
func TestBuildToolParams_ResolveLanguages(t *testing.T) {
119-
dir := t.TempDir()
119+
dir := projectTmpDir(t, "resolve-languages")
120120
testDir := filepath.Join(dir, "tools", "codeql_resolve_languages", "list_languages")
121121
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
122122
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -134,7 +134,7 @@ func TestBuildToolParams_ResolveLanguages(t *testing.T) {
134134
}
135135

136136
func TestBuildToolParams_UnknownTool(t *testing.T) {
137-
dir := t.TempDir()
137+
dir := projectTmpDir(t, "unknown-tool")
138138
testDir := filepath.Join(dir, "tools", "unknown_tool_xyz", "test1")
139139
os.MkdirAll(filepath.Join(testDir, "before"), 0o755)
140140
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
@@ -148,7 +148,7 @@ func TestBuildToolParams_UnknownTool(t *testing.T) {
148148
}
149149

150150
func TestFindFilesByExt(t *testing.T) {
151-
dir := t.TempDir()
151+
dir := projectTmpDir(t, "find-files")
152152
os.WriteFile(filepath.Join(dir, "a.ql"), []byte(""), 0o600)
153153
os.WriteFile(filepath.Join(dir, "b.ql"), []byte(""), 0o600)
154154
os.WriteFile(filepath.Join(dir, "c.txt"), []byte(""), 0o600)
@@ -189,7 +189,7 @@ func TestIsSARIFTool(t *testing.T) {
189189
}
190190

191191
func TestBuildToolParams_SARIFToolWithConfig(t *testing.T) {
192-
dir := t.TempDir()
192+
dir := projectTmpDir(t, "sarif-tool-config")
193193
testDir := filepath.Join(dir, "tools", "sarif_extract_rule", "extract_sql_injection")
194194
beforeDir := filepath.Join(testDir, "before")
195195
os.MkdirAll(beforeDir, 0o755)
@@ -221,7 +221,7 @@ func TestBuildToolParams_SARIFToolWithConfig(t *testing.T) {
221221
}
222222

223223
func TestBuildToolParams_SARIFCompareAlertsWithConfig(t *testing.T) {
224-
dir := t.TempDir()
224+
dir := projectTmpDir(t, "sarif-compare-alerts")
225225
testDir := filepath.Join(dir, "tools", "sarif_compare_alerts", "sink_overlap")
226226
beforeDir := filepath.Join(testDir, "before")
227227
os.MkdirAll(beforeDir, 0o755)

server/dist/codeql-development-mcp-server.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189516,9 +189516,6 @@ function findOverlappingAlerts(resultsA, ruleA, resultsB, ruleB, mode = "sink")
189516189516
}
189517189517
return overlaps;
189518189518
}
189519-
function diffPathMatchesSarifUri(diffPath, sarifUri) {
189520-
return urisMatch(diffPath, sarifUri);
189521-
}
189522189519
function lineInHunks(line, hunks) {
189523189520
for (const hunk of hunks) {
189524189521
const hunkEnd = hunk.startLine + Math.max(hunk.lineCount - 1, 0);
@@ -189532,6 +189529,10 @@ function diffSarifByCommits(sarif, diffFiles, refRange, granularity = "file") {
189532189529
const results = sarif.runs[0]?.results ?? [];
189533189530
const newResults = [];
189534189531
const preExistingResults = [];
189532+
const normalizedDiffEntries = diffFiles.map((df) => ({
189533+
entry: df,
189534+
normalized: normalizeUri(df.path)
189535+
}));
189535189536
for (let i = 0; i < results.length; i++) {
189536189537
const result = results[i];
189537189538
const primaryLoc = result.locations?.[0]?.physicalLocation;
@@ -189545,19 +189546,26 @@ function diffSarifByCommits(sarif, diffFiles, refRange, granularity = "file") {
189545189546
continue;
189546189547
}
189547189548
const startLine = primaryLoc?.region?.startLine;
189548-
const matchingDiff = diffFiles.find((df) => diffPathMatchesSarifUri(df.path, uri));
189549+
const normalizedUri = normalizeUri(uri);
189550+
let matchingDiff;
189551+
for (const { entry, normalized } of normalizedDiffEntries) {
189552+
if (normalized === normalizedUri || normalized.endsWith(normalizedUri) || normalizedUri.endsWith(normalized)) {
189553+
matchingDiff = entry;
189554+
break;
189555+
}
189556+
}
189549189557
let isNew = false;
189550189558
if (matchingDiff) {
189551189559
if (granularity === "file") {
189552189560
isNew = true;
189553189561
} else if (startLine !== void 0 && matchingDiff.hunks.length > 0) {
189554189562
isNew = lineInHunks(startLine, matchingDiff.hunks);
189555-
} else if (matchingDiff.hunks.length === 0) {
189563+
} else if (matchingDiff.hunks.length === 0 && !matchingDiff.hunksParsed) {
189556189564
isNew = true;
189557189565
}
189558189566
}
189559189567
const classified = {
189560-
file: matchingDiff ? matchingDiff.path : normalizeUri(uri),
189568+
file: matchingDiff ? matchingDiff.path : normalizedUri,
189561189569
line: startLine,
189562189570
resultIndex: i,
189563189571
ruleId: result.ruleId
@@ -201173,6 +201181,7 @@ function parseGitDiffOutput(diffOutput) {
201173201181
if (currentFile && line.startsWith("@@")) {
201174201182
const match = line.match(/@@ [^ ]+ \+(\d+)(?:,(\d+))? @@/);
201175201183
if (match) {
201184+
currentFile.hunksParsed = true;
201176201185
const startLine = parseInt(match[1], 10);
201177201186
const lineCount = match[2] !== void 0 ? parseInt(match[2], 10) : 1;
201178201187
if (lineCount > 0) {

server/dist/codeql-development-mcp-server.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.

server/src/lib/sarif-utils.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,14 @@ export interface SarifDiffResult {
8989

9090
/** A file changed in a git diff, with optional line ranges. */
9191
export interface DiffFileEntry {
92-
/** Changed line ranges (hunks). Empty array means file-level only. */
92+
/** Changed line ranges (hunks). Only includes hunks with lineCount > 0 (additions). */
9393
hunks: Array<{ startLine: number; lineCount: number }>;
94+
/**
95+
* Whether hunk headers were parsed for this file. When true and hunks is empty,
96+
* the diff contained only deletions (no new lines). When false/undefined and
97+
* hunks is empty, no hunk information was available (file-level match only).
98+
*/
99+
hunksParsed?: boolean;
94100
/** File path relative to the repository root. */
95101
path: string;
96102
}
@@ -863,18 +869,6 @@ export function findOverlappingAlerts(
863869
// SARIF-to-git-diff correlation
864870
// ---------------------------------------------------------------------------
865871

866-
/**
867-
* Check whether a SARIF URI matches a diff file path.
868-
*
869-
* SARIF URIs may be absolute (`file:///…`) or relative (`src/db.js`),
870-
* while git diff paths are always relative to the repo root (e.g. `src/db.js`).
871-
* We normalize both and use suffix matching so that cross-environment
872-
* comparisons work (e.g. CI vs local).
873-
*/
874-
function diffPathMatchesSarifUri(diffPath: string, sarifUri: string): boolean {
875-
return urisMatch(diffPath, sarifUri);
876-
}
877-
878872
/**
879873
* Check whether a line number falls within any of a file's changed hunks.
880874
*/
@@ -912,6 +906,13 @@ export function diffSarifByCommits(
912906
const newResults: ClassifiedResult[] = [];
913907
const preExistingResults: ClassifiedResult[] = [];
914908

909+
// Precompute normalized diff paths once to avoid re-normalizing on every
910+
// result iteration (O(results) normalize calls instead of O(results × diffFiles)).
911+
const normalizedDiffEntries = diffFiles.map(df => ({
912+
entry: df,
913+
normalized: normalizeUri(df.path),
914+
}));
915+
915916
for (let i = 0; i < results.length; i++) {
916917
const result = results[i];
917918
const primaryLoc = result.locations?.[0]?.physicalLocation;
@@ -928,25 +929,34 @@ export function diffSarifByCommits(
928929
}
929930

930931
const startLine = primaryLoc?.region?.startLine;
931-
932-
// Find matching diff file
933-
const matchingDiff = diffFiles.find(df => diffPathMatchesSarifUri(df.path, uri));
932+
const normalizedUri = normalizeUri(uri);
933+
934+
// Find matching diff file using precomputed normalized paths
935+
let matchingDiff: DiffFileEntry | undefined;
936+
for (const { entry, normalized } of normalizedDiffEntries) {
937+
if (normalized === normalizedUri || normalized.endsWith(normalizedUri) || normalizedUri.endsWith(normalized)) {
938+
matchingDiff = entry;
939+
break;
940+
}
941+
}
934942

935943
let isNew = false;
936944
if (matchingDiff) {
937945
if (granularity === 'file') {
938946
isNew = true;
939947
} else if (startLine !== undefined && matchingDiff.hunks.length > 0) {
940948
isNew = lineInHunks(startLine, matchingDiff.hunks);
941-
} else if (matchingDiff.hunks.length === 0) {
942-
// No hunk info available — treat as file-level match
949+
} else if (matchingDiff.hunks.length === 0 && !matchingDiff.hunksParsed) {
950+
// No hunk info available at all — treat as file-level match
943951
isNew = true;
944952
}
945-
// else: line granularity requested but no startLine on the result → pre-existing
953+
// else: line granularity but hunksParsed=true with empty hunks means
954+
// deletion-only diff (no new lines) → pre-existing.
955+
// Or: line granularity requested but no startLine on the result → pre-existing.
946956
}
947957

948958
const classified: ClassifiedResult = {
949-
file: matchingDiff ? matchingDiff.path : normalizeUri(uri),
959+
file: matchingDiff ? matchingDiff.path : normalizedUri,
950960
line: startLine,
951961
resultIndex: i,
952962
ruleId: result.ruleId,

server/src/tools/sarif-tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ function parseGitDiffOutput(diffOutput: string): DiffFileEntry[] {
347347
if (currentFile && line.startsWith('@@')) {
348348
const match = line.match(/@@ [^ ]+ \+(\d+)(?:,(\d+))? @@/);
349349
if (match) {
350+
currentFile.hunksParsed = true;
350351
const startLine = parseInt(match[1], 10);
351352
const lineCount = match[2] !== undefined ? parseInt(match[2], 10) : 1;
352353
if (lineCount > 0) {

server/test/src/lib/sarif-utils.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,15 +1596,41 @@ describe('diffSarifByCommits', () => {
15961596
expect(result.newResults.filter(r => r.file === 'src/db.js')).toHaveLength(1);
15971597
});
15981598

1599-
it('should treat files with empty hunks as file-level match in line mode', () => {
1599+
it('should treat files with no hunks parsed as file-level match in line mode', () => {
16001600
const sarif = createDiffTestSarif();
16011601
const diffFiles: DiffFileEntry[] = [
1602-
{ path: 'src/db.js', hunks: [] },
1602+
{ path: 'src/db.js', hunks: [], hunksParsed: false },
1603+
];
1604+
1605+
const result = diffSarifByCommits(sarif, diffFiles, 'main..HEAD', 'line');
1606+
1607+
// No hunk info at all → falls back to file-level
1608+
expect(result.newResults.filter(r => r.file === 'src/db.js')).toHaveLength(1);
1609+
});
1610+
1611+
it('should classify results as pre-existing for deletion-only diffs in line mode', () => {
1612+
const sarif = createDiffTestSarif();
1613+
// hunksParsed=true but hunks=[] means all hunks had lineCount=0 (deletion-only)
1614+
const diffFiles: DiffFileEntry[] = [
1615+
{ path: 'src/db.js', hunks: [], hunksParsed: true },
16031616
];
16041617

16051618
const result = diffSarifByCommits(sarif, diffFiles, 'main..HEAD', 'line');
16061619

1607-
// No hunk info → falls back to file-level
1620+
// Deletion-only file: no new lines → results should be pre-existing
1621+
expect(result.newResults.filter(r => r.file === 'src/db.js')).toHaveLength(0);
1622+
expect(result.preExistingResults.filter(r => r.file === 'src/db.js')).toHaveLength(1);
1623+
});
1624+
1625+
it('should classify results as new for deletion-only diffs in file mode', () => {
1626+
const sarif = createDiffTestSarif();
1627+
// In file mode, any matching file is "new" regardless of hunk details
1628+
const diffFiles: DiffFileEntry[] = [
1629+
{ path: 'src/db.js', hunks: [], hunksParsed: true },
1630+
];
1631+
1632+
const result = diffSarifByCommits(sarif, diffFiles, 'main..HEAD', 'file');
1633+
16081634
expect(result.newResults.filter(r => r.file === 'src/db.js')).toHaveLength(1);
16091635
});
16101636

server/test/src/tools/sarif-tools.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,32 @@ describe('SARIF Tools', () => {
653653
const newFiles = parsed.newResults.map((r: any) => r.file);
654654
expect(newFiles).toContain('src/db.js');
655655
});
656+
657+
it('should classify results as pre-existing for deletion-only diffs in line mode', async () => {
658+
// Deletion-only hunk: @@ -10,3 +10,0 @@ means 3 lines removed, 0 added
659+
mockExecuteCLICommand.mockResolvedValue({
660+
success: true,
661+
stdout: [
662+
'diff --git a/src/db.js b/src/db.js',
663+
'--- a/src/db.js',
664+
'+++ b/src/db.js',
665+
'@@ -10,3 +10,0 @@',
666+
].join('\n'),
667+
stderr: '',
668+
});
669+
670+
const result = await handlers.sarif_diff_by_commits({
671+
sarifPath: testSarifPath,
672+
refRange: 'main..HEAD',
673+
granularity: 'line',
674+
});
675+
const parsed = JSON.parse(result.content[0].text);
676+
677+
expect(parsed.granularity).toBe('line');
678+
// src/db.js had only deletions — no new lines, so result at line 42 is pre-existing
679+
const newInDb = parsed.newResults.filter((r: any) => r.file === 'src/db.js');
680+
expect(newInDb).toHaveLength(0);
681+
});
656682
});
657683
});
658684
});

0 commit comments

Comments
 (0)