Improve PR issue reference parsing and add edge-case tests

swadhinbiswas committed 8 months ago
df9f4e4
1 parent 8424830
commit df9f4e4
2 files changed
+57 -9
▼src/lib/pr-issue-linking.ts
+28-9
42
42
number: number;
43
43
type: "closes" | "fixes" | "relates";
44
44
}[] {
45
- const references: { number: number; type: "closes" | "fixes" | "relates" }[] = [];
45
+ const referenceTypeByNumber = new Map<number, "closes" | "fixes" | "relates">();
46
+ const setReference = (number: number, type: "closes" | "fixes" | "relates") => {
47
+ const existing = referenceTypeByNumber.get(number);
48
+ if (!existing) {
49
+ referenceTypeByNumber.set(number, type);
50
+ return;
51
+ }
52
+
53
+ // Preserve stronger semantic link types over generic "relates".
54
+ if (existing === "relates" && (type === "closes" || type === "fixes")) {
55
+ referenceTypeByNumber.set(number, type);
56
+ }
57
+ };
46
58
47
59
// Pattern: (keyword) #number or (keyword) repo#number
60
+ const scopedIssueRef = "(?:[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+)?#(\\d+)";
48
61
for (const keyword of CLOSING_KEYWORDS) {
49
- const regex = new RegExp(`${keyword}\\s+#(\\d+)`, "gi");
62
+ const regex = new RegExp(`\\b${keyword}\\b\\s+${scopedIssueRef}\\b`, "gi");
50
63
let match;
51
64
while ((match = regex.exec(text)) !== null) {
52
65
const type = keyword.startsWith("fix") ? "fixes" : "closes";
53
- references.push({ number: parseInt(match[1]), type });
66
+ const number = Number.parseInt(match[1], 10);
67
+ if (Number.isInteger(number)) {
68
+ setReference(number, type);
69
+ }
54
70
}
55
71
}
56
72
57
- // Pattern: just #number (relates)
58
- const relatesRegex = /#(\d+)/g;
73
+ // Pattern: just #number or owner/repo#number (relates)
74
+ const relatesRegex = new RegExp(`${scopedIssueRef}\\b`, "g");
59
75
let match;
60
76
while ((match = relatesRegex.exec(text)) !== null) {
61
- const num = parseInt(match[1]);
62
- if (!references.some(r => r.number === num)) {
63
- references.push({ number: num, type: "relates" });
77
+ const number = Number.parseInt(match[1], 10);
78
+ if (Number.isInteger(number)) {
79
+ setReference(number, "relates");
64
80
}
65
81
}
66
82
67
- return references;
83
+ return Array.from(referenceTypeByNumber.entries()).map(([number, type]) => ({
84
+ number,
85
+ type,
86
+ }));
68
87
}
69
88
70
89
/**
71
90
▼tests/unit/pr-issue-linking.test.ts
+29-0
1
+import { describe, expect, it } from "vitest";
2
+import { parseIssueReferences } from "@/lib/pr-issue-linking";
3
+
4
+describe("parseIssueReferences", () => {
5
+ it("parses basic closing and related references", () => {
6
+ const refs = parseIssueReferences("Fixes #12 and also touches #20");
7
+ expect(refs).toContainEqual({ number: 12, type: "fixes" });
8
+ expect(refs).toContainEqual({ number: 20, type: "relates" });
9
+ });
10
+
11
+ it("parses scoped repository references for closing keywords", () => {
12
+ const refs = parseIssueReferences("closes acme/platform#101");
13
+ expect(refs).toContainEqual({ number: 101, type: "closes" });
14
+ });
15
+
16
+ it("does not downgrade closes/fixes to relates when both patterns match", () => {
17
+ const refs = parseIssueReferences("Resolves #88 and #88 is part of rollout");
18
+ const only = refs.filter((r) => r.number === 88);
19
+ expect(only).toHaveLength(1);
20
+ expect(only[0]).toEqual({ number: 88, type: "closes" });
21
+ });
22
+
23
+ it("supports multiple closing keywords in one body", () => {
24
+ const refs = parseIssueReferences("Fixes #3, closes org/repo#4, and relates to #5");
25
+ expect(refs).toContainEqual({ number: 3, type: "fixes" });
26
+ expect(refs).toContainEqual({ number: 4, type: "closes" });
27
+ expect(refs).toContainEqual({ number: 5, type: "relates" });
28
+ });
29
+});
0
30