edsger 0.6.10 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,100 @@ async function* prompt(reviewPrompt) {
17
17
  yield userMessage(reviewPrompt);
18
18
  await new Promise((res) => setTimeout(res, 10000));
19
19
  }
20
+ /**
21
+ * Parse unified diff patch to build a mapping from file line numbers to diff positions
22
+ *
23
+ * GitHub API uses "position" parameter which is the line number in the unified diff:
24
+ * - Position starts at 1 for the first line AFTER the @@ hunk header
25
+ * - Increments for every line (context, additions, deletions)
26
+ * - Continues through all hunks in the file
27
+ *
28
+ * Example unified diff:
29
+ * ```
30
+ * @@ -1,3 +1,4 @@ <- not counted in position
31
+ * context line <- position 1, file line 1
32
+ * -old line <- position 2, (deleted line, no file line number)
33
+ * +new line <- position 3, file line 2
34
+ * +another new <- position 4, file line 3
35
+ * context <- position 5, file line 4
36
+ * ```
37
+ *
38
+ * Returns: Map<file line number, diff position>
39
+ */
40
+ function buildLineToPositionMap(patch) {
41
+ const lineToPosition = new Map();
42
+ const lines = patch.split('\n');
43
+ let position = 0; // Position in the diff (starts at 1 after first line after @@)
44
+ let currentNewLine = 0; // Current line number in the new file (RIGHT side)
45
+ for (const line of lines) {
46
+ // Parse hunk headers like @@ -1,7 +1,7 @@
47
+ // This extracts the starting line number for the new file (+side)
48
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
49
+ if (hunkMatch) {
50
+ currentNewLine = parseInt(hunkMatch[1], 10);
51
+ // Don't increment position for the @@ line itself
52
+ continue;
53
+ }
54
+ // Every line after @@ increments the position counter
55
+ position++;
56
+ // Map line numbers based on line type
57
+ if (line.startsWith('+')) {
58
+ // Addition: this line exists in the new file at currentNewLine
59
+ lineToPosition.set(currentNewLine, position);
60
+ currentNewLine++;
61
+ }
62
+ else if (line.startsWith('-')) {
63
+ // Deletion: this line existed in old file, not in new file
64
+ // Position still increments, but currentNewLine does not
65
+ // We can't comment on this line using file line numbers
66
+ }
67
+ else if (!line.startsWith('\\')) {
68
+ // Context line (no prefix): exists in both old and new file
69
+ lineToPosition.set(currentNewLine, position);
70
+ currentNewLine++;
71
+ }
72
+ // Lines starting with '\' (like "") are metadata
73
+ }
74
+ return lineToPosition;
75
+ }
76
+ /**
77
+ * Find the diff position for a target line number, or the closest nearby position
78
+ *
79
+ * @param targetLine - The file line number we want to comment on
80
+ * @param lineToPosition - Map of file line numbers to diff positions
81
+ * @returns Object with position and actual line number, or null if not found
82
+ */
83
+ function findClosestPosition(targetLine, lineToPosition) {
84
+ // Check if exact line exists in the diff
85
+ if (lineToPosition.has(targetLine)) {
86
+ return {
87
+ position: lineToPosition.get(targetLine),
88
+ actualLine: targetLine,
89
+ };
90
+ }
91
+ // Try to find a nearby line within ±10 lines range
92
+ const range = 10;
93
+ for (let offset = 1; offset <= range; offset++) {
94
+ // Try below first (more likely to be relevant for review comments)
95
+ const lineBelow = targetLine + offset;
96
+ if (lineToPosition.has(lineBelow)) {
97
+ return {
98
+ position: lineToPosition.get(lineBelow),
99
+ actualLine: lineBelow,
100
+ };
101
+ }
102
+ // Try above
103
+ const lineAbove = targetLine - offset;
104
+ if (lineToPosition.has(lineAbove)) {
105
+ return {
106
+ position: lineToPosition.get(lineAbove),
107
+ actualLine: lineAbove,
108
+ };
109
+ }
110
+ }
111
+ // No valid position found within range
112
+ return null;
113
+ }
20
114
  /**
21
115
  * Main code review function
22
116
  */
@@ -179,16 +273,81 @@ export const reviewPullRequest = async (options, config) => {
179
273
  if (verbose) {
180
274
  logInfo(`Creating GitHub review with ${comments.length} comments...`);
181
275
  }
182
- // Create review with inline comments
183
- const reviewComments = comments.map((comment) => ({
184
- path: comment.file || comment.path,
185
- line: comment.line,
186
- body: comment.comment || comment.body,
187
- }));
276
+ // Build a map of file paths to their line-to-position mappings
277
+ const fileLineToPosition = new Map();
278
+ for (const file of context.files) {
279
+ if (file.patch) {
280
+ fileLineToPosition.set(file.filename, buildLineToPositionMap(file.patch));
281
+ }
282
+ }
283
+ // Get the latest commit SHA from the PR for the review
284
+ const commitId = context.prData.head.sha;
285
+ // Create review with inline comments using unified diff positions
286
+ const reviewComments = [];
287
+ for (const comment of comments) {
288
+ const filePath = comment.file || comment.path;
289
+ const requestedLine = comment.line;
290
+ const lineToPosition = fileLineToPosition.get(filePath);
291
+ if (!lineToPosition) {
292
+ if (verbose) {
293
+ logInfo(`⚠️ Skipping comment for ${filePath}:${requestedLine} - file has no diff`);
294
+ }
295
+ continue;
296
+ }
297
+ // Find the diff position for this line number
298
+ const positionResult = findClosestPosition(requestedLine, lineToPosition);
299
+ if (positionResult === null) {
300
+ if (verbose) {
301
+ logInfo(`⚠️ Skipping comment for ${filePath}:${requestedLine} - line not in diff range`);
302
+ }
303
+ continue;
304
+ }
305
+ let commentBody = comment.comment || comment.body;
306
+ // If we had to adjust the line number, add a note
307
+ if (positionResult.actualLine !== requestedLine) {
308
+ commentBody = `**Note**: Comment originally for line ${requestedLine}, adjusted to line ${positionResult.actualLine} (nearest line in diff).\n\n${commentBody}`;
309
+ if (verbose) {
310
+ logInfo(` Adjusted ${filePath}:${requestedLine} → line ${positionResult.actualLine} (position ${positionResult.position})`);
311
+ }
312
+ }
313
+ reviewComments.push({
314
+ path: filePath,
315
+ position: positionResult.position,
316
+ body: commentBody,
317
+ });
318
+ }
319
+ // If all comments were filtered out, just create a review with the summary
320
+ if (reviewComments.length === 0) {
321
+ if (verbose) {
322
+ logInfo(`⚠️ All ${comments.length} comments were filtered out (invalid line numbers)`);
323
+ logInfo('Creating review with summary only...');
324
+ }
325
+ const review = await octokit.pulls.createReview({
326
+ owner: context.owner,
327
+ repo: context.repo,
328
+ pull_number: context.pullRequestNumber,
329
+ event: 'COMMENT',
330
+ body: (summary || overall_assessment || 'Code review completed.') +
331
+ '\n\n**Note**: Some review comments could not be posted because they referenced lines not present in the diff.',
332
+ });
333
+ return {
334
+ featureId,
335
+ status: 'success',
336
+ message: 'Code review completed - comments filtered due to invalid line numbers',
337
+ reviewId: review.data.id,
338
+ reviewUrl: review.data.html_url,
339
+ commentsCount: 0,
340
+ summary: summary || 'Code review completed (comments filtered)',
341
+ };
342
+ }
343
+ if (verbose) {
344
+ logInfo(`📝 Posting ${reviewComments.length} validated comments (${comments.length - reviewComments.length} filtered out)`);
345
+ }
188
346
  const review = await octokit.pulls.createReview({
189
347
  owner: context.owner,
190
348
  repo: context.repo,
191
349
  pull_number: context.pullRequestNumber,
350
+ commit_id: commitId, // Required for position-based comments
192
351
  event: 'COMMENT',
193
352
  body: summary ||
194
353
  overall_assessment ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"