Skip to content

Commit 3ca5337

Browse files
RealDiligentRealDiligentloopover-orb[bot]
authored
fix(review): make resolveInlineCommentAnchor enforce its own no-422 contract (#8455)
* fix(review): make resolveInlineCommentAnchor enforce its own no-422 contract The fallback branch returned a single-line anchor built from start whenever the full-range check failed, without ever verifying start itself is a commentable RIGHT-side line -- so an un-postable anchor was returned as if it were the documented fail-safe. It held only because selectAnchoredInlineFindings pre-filters on that same line, an undocumented precondition this exported, independently-tested pure function neither enforced nor mentioned. Adds an anchorable flag, false only when start is not a valid RIGHT-side line, and has selectInlineComments drop such findings instead of trusting the pre-filter alone. No behavior change for any finding whose start line is valid. Closes #8352 * fix(review): write added lines with LF so git diff --check passes The 'changes' job runs git diff --check, which flags a CR at the end of any added line. inline-comment-range.ts and its test are committed with CRLF, so matching their existing endings made every line this PR adds fail that check. Added lines now use LF (untouched lines keep their existing endings, so the diff stays minimal); no content change. --------- Co-authored-by: RealDiligent <nft.gold.eth@gmail.com> Co-authored-by: loopover-orb[bot] <296761690+loopover-orb[bot]@users.noreply.github.com>
1 parent ac5ffbd commit 3ca5337

3 files changed

Lines changed: 61 additions & 30 deletions

File tree

src/review/inline-comment-range.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,22 @@ export function rightLinesByPath(
3333
}
3434

3535
/** Resolve the GitHub inline-comment anchor for a finding. Multi-line ONLY when every line in [start,end] is
36-
* commentable on the RIGHT side; otherwise downgrade to the single start line (fail-safe, no 422). */
37-
export function resolveInlineCommentAnchor(
38-
finding: Pick<InlineFinding, "path" | "line" | "endLine">,
39-
rightLines: Map<string, Set<number>>,
40-
): { start: number; end: number; multiLine: boolean } {
41-
const { start, end } = parseInlineLineRange(finding);
42-
const validLines = rightLines.get(finding.path);
43-
if (!validLines || !everyLineInSet(start, end, validLines)) {
44-
return { start, end: start, multiLine: false };
45-
}
46-
if (end > start) return { start, end, multiLine: true };
47-
return { start, end: start, multiLine: false };
36+
* commentable on the RIGHT side; otherwise downgrade to the single start line (fail-safe, no 422).
37+
* `anchorable` is false when `start` ITSELF is not a commentable RIGHT-side line -- the fallback used to
38+
* return that start line anyway, presenting an un-postable anchor as a safe one and leaving the "no 422"
39+
* promise to an undocumented caller-side pre-filter. Callers must drop an `anchorable: false` finding. */
40+
export function resolveInlineCommentAnchor(
41+
finding: Pick<InlineFinding, "path" | "line" | "endLine">,
42+
rightLines: Map<string, Set<number>>,
43+
): { start: number; end: number; multiLine: boolean; anchorable: boolean } {
44+
const { start, end } = parseInlineLineRange(finding);
45+
const validLines = rightLines.get(finding.path);
46+
if (!validLines || !validLines.has(start)) {
47+
return { start, end: start, multiLine: false, anchorable: false };
48+
}
49+
if (!everyLineInSet(start, end, validLines)) {
50+
return { start, end: start, multiLine: false, anchorable: true };
51+
}
52+
if (end > start) return { start, end, multiLine: true, anchorable: true };
53+
return { start, end: start, multiLine: false, anchorable: true };
4854
}

src/review/inline-comments.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,16 @@ export function selectInlineComments(
135135
});
136136
const addedLines = addedLinesByPath(files);
137137
const rightLines = rightLinesByPath(files);
138-
return selected.map((finding) => {
138+
const comments: ReviewInlineComment[] = [];
139+
for (const finding of selected) {
139140
const anchor = resolveInlineCommentAnchor(finding, rightLines);
141+
// Defense in depth: honor the resolver's own verdict rather than relying on the caller-side
142+
// precondition -- an un-anchorable finding would 422 the whole review request.
143+
/* v8 ignore next -- selectAnchoredInlineFindings already drops findings whose line is not a
144+
commentable RIGHT-side line, and parseInlineLineRange uses that same line as `start`, so this
145+
guard is unreachable today; it exists so a future selection change cannot silently reintroduce
146+
the 422 this resolver is meant to prevent. */
147+
if (!anchor.anchorable) continue;
140148
const anchoredFinding: InlineFinding =
141149
anchor.multiLine ? finding : { ...finding, endLine: undefined };
142150
const comment: ReviewInlineComment = {
@@ -149,8 +157,9 @@ export function selectInlineComments(
149157
comment.start_line = anchor.start;
150158
comment.start_side = "RIGHT";
151159
}
152-
return comment;
153-
});
160+
comments.push(comment);
161+
}
162+
return comments;
154163
}
155164

156165
/** Post the model's inline findings as ONE quiet, non-blocking review (`event: COMMENT`) on the PR. Fully

test/unit/inline-comment-range.test.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,51 @@ describe("resolveInlineCommentAnchor (#2141)", () => {
4747
it("emits a multi-line anchor when every line in the range is commentable", () => {
4848
const rightLines = rightLinesByPath(files);
4949
expect(resolveInlineCommentAnchor({ path: "src/a.ts", line: 1, endLine: 3 }, rightLines)).toEqual({
50-
start: 1,
51-
end: 3,
52-
multiLine: true,
50+
start: 1,
51+
end: 3,
52+
multiLine: true,
53+
anchorable: true,
5354
});
5455
});
5556

5657
it("downgrades to the start line when any line in the range is not commentable", () => {
5758
const rightLines = rightLinesByPath([{ path: "src/a.ts", payload: { patch: mixedPatch } }]);
5859
expect(resolveInlineCommentAnchor({ path: "src/a.ts", line: 2, endLine: 99 }, rightLines)).toEqual({
59-
start: 2,
60-
end: 2,
61-
multiLine: false,
60+
start: 2,
61+
end: 2,
62+
multiLine: false,
63+
anchorable: true,
6264
});
6365
});
6466

65-
it("downgrades when the file path is missing from the RIGHT-side line map", () => {
66-
expect(resolveInlineCommentAnchor({ path: "src/missing.ts", line: 1, endLine: 3 }, new Map())).toEqual({
67-
start: 1,
68-
end: 1,
69-
multiLine: false,
70-
});
67+
it("reports NOT anchorable when the file path is missing from the RIGHT-side line map (#8352)", () => {
68+
// Previously this returned start line 1 as a "safe" single-line anchor even though no line on this
69+
// path was ever validated -- an un-postable anchor presented as postable (the 422 this guards).
70+
expect(resolveInlineCommentAnchor({ path: "src/missing.ts", line: 1, endLine: 3 }, new Map())).toEqual({
71+
start: 1,
72+
end: 1,
73+
multiLine: false,
74+
anchorable: false,
75+
});
76+
});
77+
78+
it("reports NOT anchorable when the start line itself is not commentable though the path IS mapped (#8352)", () => {
79+
const rightLines = new Map([["src/a.ts", new Set([20, 21, 22])]]);
80+
expect(resolveInlineCommentAnchor({ path: "src/a.ts", line: 5, endLine: 10 }, rightLines)).toEqual({
81+
start: 5,
82+
end: 5,
83+
multiLine: false,
84+
anchorable: false,
85+
});
7186
});
7287

7388
it("keeps a single-line anchor when the range collapses to one commentable line", () => {
7489
const rightLines = rightLinesByPath(files);
7590
expect(resolveInlineCommentAnchor({ path: "src/a.ts", line: 2 }, rightLines)).toEqual({
76-
start: 2,
77-
end: 2,
78-
multiLine: false,
91+
start: 2,
92+
end: 2,
93+
multiLine: false,
94+
anchorable: true,
7995
});
8096
});
8197
});

0 commit comments

Comments
 (0)