@jphutchins/code-review 0.1.0-alpha.8 → 0.1.0-alpha.9
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.
- package/dist/index.js +105 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,6 +89,10 @@ var drop = (reason) => ({
|
|
|
89
89
|
kind: "drop",
|
|
90
90
|
reason
|
|
91
91
|
});
|
|
92
|
+
var keep = (reason) => ({
|
|
93
|
+
kind: "keep",
|
|
94
|
+
reason
|
|
95
|
+
});
|
|
92
96
|
var parseHunk = (patch) => {
|
|
93
97
|
const rawLines = patch.split("\n");
|
|
94
98
|
const lines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
|
|
@@ -128,10 +132,12 @@ var validatePatch = (patch, fileLines) => {
|
|
|
128
132
|
const removedCount = body.filter((l) => l.kind === "removed").length;
|
|
129
133
|
const addedCount = body.filter((l) => l.kind === "added").length;
|
|
130
134
|
if (removedCount === 0 && addedCount === 0) return drop("hunk contains no changes");
|
|
131
|
-
if (removedCount === 0)
|
|
135
|
+
if (removedCount === 0) {
|
|
136
|
+
return keep("pure insertion applies cleanly but has no removed range to anchor a suggestion");
|
|
137
|
+
}
|
|
132
138
|
const { firstRemoved, lastRemoved } = removedRange(body, oldStart);
|
|
133
139
|
if (firstRemoved === null || lastRemoved === null) return drop("malformed hunk body");
|
|
134
|
-
return { startLine: firstRemoved, endLine: lastRemoved };
|
|
140
|
+
return { kind: "anchored", startLine: firstRemoved, endLine: lastRemoved };
|
|
135
141
|
};
|
|
136
142
|
var patchToSuggestion = (patch) => {
|
|
137
143
|
const parsed = parseHunk(patch);
|
|
@@ -164,12 +170,14 @@ var severityEmoji = (s) => {
|
|
|
164
170
|
};
|
|
165
171
|
var EMBED_LIMIT = 4e4;
|
|
166
172
|
var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
|
|
167
|
-
var
|
|
168
|
-
const b64 = Buffer.from(JSON.stringify(
|
|
173
|
+
var encodeMarker = (document, jsonUrl, limit) => {
|
|
174
|
+
const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
|
|
169
175
|
const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
|
|
170
176
|
return marker ? `${AGENTS_STOP_DIRECTIVE}
|
|
171
177
|
${marker}` : "";
|
|
172
178
|
};
|
|
179
|
+
var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
|
|
180
|
+
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
173
181
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
174
182
|
var projectPatch = (patch) => {
|
|
175
183
|
if (patch === void 0) return { kind: "none" };
|
|
@@ -330,8 +338,8 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
330
338
|
const { inDiff, strays } = partitionFindings(findings, index);
|
|
331
339
|
const eta = new Eta({ autoTrim: false });
|
|
332
340
|
const modelsText = formatModels(models);
|
|
333
|
-
const pointer = context.findingsPointer ?? (fullFindings ? findingsPointer(fullFindings, jsonUrl) : "");
|
|
334
341
|
const comments = inDiff.map((f) => {
|
|
342
|
+
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
335
343
|
const comment = {
|
|
336
344
|
path: f.path,
|
|
337
345
|
line: f.end_line,
|
|
@@ -868,6 +876,84 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
868
876
|
}
|
|
869
877
|
}
|
|
870
878
|
};
|
|
879
|
+
var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login} originalCommit{oid}}}}}}}}";
|
|
880
|
+
var MINIMIZE_COMMENT_MUTATION = "mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:OUTDATED}){minimizedComment{isMinimized}}}";
|
|
881
|
+
var supersededCommentId = (c, headSha, logins) => {
|
|
882
|
+
if (typeof c !== "object" || c === null) return null;
|
|
883
|
+
const o = c;
|
|
884
|
+
const login = o.author?.login;
|
|
885
|
+
const oid = o.originalCommit?.oid;
|
|
886
|
+
return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) && typeof oid === "string" && oid !== headSha ? o.id : null;
|
|
887
|
+
};
|
|
888
|
+
var supersededBotCommentIds = (raw, headSha, botLogin) => {
|
|
889
|
+
let parsed;
|
|
890
|
+
try {
|
|
891
|
+
parsed = JSON.parse(raw);
|
|
892
|
+
} catch {
|
|
893
|
+
return { ids: [], truncated: false };
|
|
894
|
+
}
|
|
895
|
+
const conn = parsed.data?.repository?.pullRequest?.reviewThreads;
|
|
896
|
+
const truncated = conn?.pageInfo?.hasNextPage === true;
|
|
897
|
+
const nodes = conn?.nodes;
|
|
898
|
+
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
899
|
+
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
900
|
+
const ids = nodes.flatMap((t4) => {
|
|
901
|
+
const cnodes = t4.comments?.nodes;
|
|
902
|
+
return Array.isArray(cnodes) ? cnodes.map((c) => supersededCommentId(c, headSha, logins)).filter((id) => id !== null) : [];
|
|
903
|
+
});
|
|
904
|
+
return { ids, truncated };
|
|
905
|
+
};
|
|
906
|
+
var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi) => {
|
|
907
|
+
const slash = repo.indexOf("/");
|
|
908
|
+
if (slash <= 0) return;
|
|
909
|
+
const owner = repo.slice(0, slash);
|
|
910
|
+
const name = repo.slice(slash + 1);
|
|
911
|
+
let raw;
|
|
912
|
+
try {
|
|
913
|
+
raw = await ghApi([
|
|
914
|
+
"graphql",
|
|
915
|
+
"-f",
|
|
916
|
+
`query=${REVIEW_THREAD_COMMENTS_QUERY}`,
|
|
917
|
+
"-f",
|
|
918
|
+
`owner=${owner}`,
|
|
919
|
+
"-f",
|
|
920
|
+
`name=${name}`,
|
|
921
|
+
"-F",
|
|
922
|
+
`pr=${String(prNumber)}`
|
|
923
|
+
]);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
process.stderr.write(
|
|
926
|
+
`Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
|
|
927
|
+
`
|
|
928
|
+
);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const { ids, truncated } = supersededBotCommentIds(raw, headSha, botLogin);
|
|
932
|
+
if (truncated) {
|
|
933
|
+
process.stderr.write(
|
|
934
|
+
`Note: PR #${String(prNumber)} has more than 100 review threads \u2014 only the first 100 were scanned for stale bot comments
|
|
935
|
+
`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
let minimized = 0;
|
|
939
|
+
for (const id of ids) {
|
|
940
|
+
try {
|
|
941
|
+
await ghApi(["graphql", "-f", `query=${MINIMIZE_COMMENT_MUTATION}`, "-f", `id=${id}`]);
|
|
942
|
+
minimized += 1;
|
|
943
|
+
} catch (err) {
|
|
944
|
+
process.stderr.write(
|
|
945
|
+
`Warning: failed to minimize a stale review comment on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
|
|
946
|
+
`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (minimized > 0) {
|
|
951
|
+
process.stderr.write(
|
|
952
|
+
`Minimized ${String(minimized)} stale inline comment(s) from superseded reviews on PR #${String(prNumber)}
|
|
953
|
+
`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
871
957
|
var post = async (input, ghApi = runGhApi) => {
|
|
872
958
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
873
959
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
@@ -966,7 +1052,8 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
966
1052
|
const { comments: rawComments, strays } = buildInlineComments(findings.findings, diff, {
|
|
967
1053
|
inlineTemplate,
|
|
968
1054
|
models: envelope.models.map((m) => m.model),
|
|
969
|
-
|
|
1055
|
+
findings,
|
|
1056
|
+
jsonUrl: input.jsonUrl
|
|
970
1057
|
});
|
|
971
1058
|
const { comments, longFiles } = checkLongSuggestions(rawComments);
|
|
972
1059
|
for (const wf of longFiles) {
|
|
@@ -1034,6 +1121,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1034
1121
|
`Posted ${String(comments.length)} inline comments on PR #${String(prNumber)}
|
|
1035
1122
|
`
|
|
1036
1123
|
);
|
|
1124
|
+
await minimizeSupersededComments(input.repo, prNumber, input.headSha, input.botLogin, ghApi);
|
|
1037
1125
|
if (stickyRef !== null) {
|
|
1038
1126
|
const confirmedDisposition = {
|
|
1039
1127
|
kind: "posted",
|
|
@@ -1836,19 +1924,23 @@ var validateFinding = (finding, repoRoot) => {
|
|
|
1836
1924
|
return withoutPatch(finding);
|
|
1837
1925
|
}
|
|
1838
1926
|
const result = validatePatch(finding.patch, lines);
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1927
|
+
switch (result.kind) {
|
|
1928
|
+
case "anchored":
|
|
1929
|
+
return { ...finding, start_line: result.startLine, end_line: result.endLine };
|
|
1930
|
+
case "keep":
|
|
1931
|
+
return finding;
|
|
1932
|
+
case "drop":
|
|
1933
|
+
process.stderr.write(
|
|
1934
|
+
`validate-patches: ${finding.path}:${String(finding.start_line)}: ${result.reason} \u2014 dropping patch
|
|
1842
1935
|
`
|
|
1843
|
-
|
|
1844
|
-
|
|
1936
|
+
);
|
|
1937
|
+
return withoutPatch(finding);
|
|
1845
1938
|
}
|
|
1846
|
-
return { ...finding, start_line: result.startLine, end_line: result.endLine };
|
|
1847
1939
|
};
|
|
1848
1940
|
var validatePatchesCmd = defineCommand({
|
|
1849
1941
|
meta: {
|
|
1850
1942
|
name: "validate-patches",
|
|
1851
|
-
description: "Validate each finding's patch against the real PR-head tree, aligning the finding's range
|
|
1943
|
+
description: "Validate each finding's patch against the real PR-head tree, aligning the finding's range and keeping the patch when it anchors, keeping it unaligned when it's a pure insertion, or dropping it when it doesn't apply (issue #10)"
|
|
1852
1944
|
},
|
|
1853
1945
|
args: {
|
|
1854
1946
|
findings: {
|