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
Showing 2 changed files+57-9
▼src/lib/pr-issue-linking.ts
+28-9
| @@ -42,29 +42,48 @@ export function parseIssueReferences(text: string): { | ||
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
| @@ -0,0 +1,29 @@ | ||
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 | |