@shiyuphan/follow-me 0.5.1 → 0.7.1
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/README.md +9 -3
- package/dist/src/cli.js +112 -19
- package/dist/src/comments.js +6 -1
- package/dist/src/coverage.js +397 -0
- package/dist/src/definition.js +226 -0
- package/dist/src/git.js +87 -13
- package/dist/src/host.js +4 -0
- package/dist/src/plan.js +12 -1
- package/dist/src/prompt.js +80 -23
- package/dist/src/server.js +65 -8
- package/dist/src/session.js +33 -0
- package/dist/src/types.js +4 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-8UsLPznO.js → abnfDiagram-WEYOIA3C-cHEJvcBn.js} +1 -1
- package/dist/ui/dist/assets/{arc-vovjGalz.js → arc-xolFyc0v.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-BBMK-PUu.js → architecture-WOLXFQ4H-B3hvvNV2.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-Bw_ehaQk.js → architectureDiagram-ZHVYKT53-BNTMbPJW.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-CAmD0wKf.js → blockDiagram-RJAE7QA3-NYY6qeuY.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-DCP-3zgt.js → c4Diagram-XDPPPE54-B_bNpXHQ.js} +1 -1
- package/dist/ui/dist/assets/channel-DPDY0XJP.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-BiHSeJMu.js → chunk-2E7WHVMD-BTet9STn.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-CR3lHKf_.js → chunk-4EMNKR2T-qoQt4Fpx.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-CtaoloTT.js → chunk-55XCU76K-C3I6EB-H.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-eABdc7fu.js → chunk-7S6XSTFO-CL-9s_8p.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-CPlbJWi-.js → chunk-AQDTSKZB-DHoGnGyP.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-YiMuxsb-.js → chunk-DSNLSHJZ-Bk8WO5JF.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-1VcdL78a.js → chunk-HAGFOTED-CwAC85lg.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-u-sr_0Qu.js → chunk-HNE4Q36P-CIllywWr.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-C_QlWSze.js → chunk-NQOKRVIF-DDlpyYiH.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-CNuZhsbU.js → chunk-UHW7PFBP-Cxdndcon.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-DAHWbLqU.js → chunk-VQ5W62QI-CL9d9_iJ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-DWQnxFCd.js → chunk-WQQL4CDK-C4kzN8QU.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-B0lGgSOR.js → chunk-XXDRQBXY-Cv_2FSiY.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-BDw7efkN.js → chunk-YZO67HFG-oHiuYaCJ.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-B5wfHsZv.js → chunk-ZAUX2LTX-Cy_8d0XT.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR--rWtYtAG.js → chunk-ZCHMQSOR-piIpIcQi.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-BKuQ_xoS.js → classDiagram-v2-4MYWR3UA-DNKRIujO.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-Dk5fIpU0.js → cose-bilkent-JH36ORCC-BWKTGCHY.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-DIykJb2g.js → cynefin-EF2NZ3EQ-yx7KKkAk.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-Cg8tBqUb.js → cynefinDiagram-WF24UDRP-BTnZ7QVL.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-BC_tnnjz.js → dagre-NK4GIZVX-CPh5Zn5I.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-B_MQGixU.js → diagram-CBK3MWTC-pI6NFdrD.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-DO681pZ5.js → diagram-I5XIIV2D-DUIfARoS.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-D14jLzU6.js → diagram-ODIL2UCW-lafSPvAw.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-cML48gUS.js → diagram-PFBEJUXE-u1lZas3a.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-DSoq8jFk.js → diagram-RJA3I4VV-mc0GLMHo.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-CaJ4pvnl.js → diagram-TYUNFSNR-1NRbBrZW.js} +1 -1
- package/dist/ui/dist/assets/{dist-D3j-DODP.js → dist-CHZ9mrl_.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-CyRpjSix.js → ebnfDiagram-V4P3TTPT-DdStjTDc.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-ICJdzVFL.js → elk-QONTBCTQ-BzRoFO45.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-B57hNTfw.js → erDiagram-KYQIGQAA-DlMkgQxj.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BG8tUVIQ.js → eventmodeling-K75KTNOO-BRYIOrod.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-Czt6Klik.js → ganttDiagram-R7GPNTJY-D8fQZ7Ah.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-De_ZWJLj.js → gitGraph-VSP46ZUC-C7ZydyWX.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-BMCJoOnT.js → gitGraphDiagram-PDYL7XHY-DCItam0v.js} +1 -1
- package/dist/ui/dist/assets/index-8BehwQ7E.js +134 -0
- package/dist/ui/dist/assets/index-mC2OW2Av.css +1 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-D75H9kUq.js → info-OHQRW6UA-C3KneBKT.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-Bsm_I9dt.js → infoDiagram-MVDRKAX5-6wK09D32.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-98xZAJ9y.js → ishikawaDiagram-HPEHV2GE-DTP5C8VX.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BWVGuc0P.js → journeyDiagram-BOSHW7O7-B--UIMtq.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DgUE_KDd.js → kanban-definition-FNSA4HC5-Ku4N9QGt.js} +1 -1
- package/dist/ui/dist/assets/{line-i__8Rvas.js → line-C4-H5h1-.js} +1 -1
- package/dist/ui/dist/assets/{linear-BXKj0Fip.js → linear-DhAID3hm.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-DxvaorTa.js → mermaid-parser.core-CMPNc-u6.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-ujYTsRZz.js → mermaid.core-Nd_xOg2y.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B7QD6sjk.js → mindmap-definition-6OY57UG3-BlVsym5h.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-B6aIwfD1.js → packet-JDAUHWVQ-rsk7SM7S.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-_RStHj-a.js → pegDiagram-ECIUJNBX-C0L20M8z.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-CNLC-pTe.js → pie-XZMESJXO-B23-iZSN.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-0uT7MT-u.js → pieDiagram-GNMPY74T-BqFhe4Oh.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-DtxziK1i.js → prism-core-DYsiqJeR.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-CTMKb5hl.js → quadrantDiagram-D57K4MC3-BKP75B-m.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-D53h6I0N.js → radar-ABXABTNO-BrEk2mQ7.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-CwjhW4mc.js → railroad-I3PHUGI6-wWO4Eapc.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DDbQ3RmB.js → railroad-abnf-LNEFI6M7-DfY-IPvE.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-Am7YJAqO.js → railroad-ebnf-DWYD2UWJ-19wedWGk.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Ci32KoVW.js → railroad-peg-JR7BVNR7-9vIslnoJ.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-BPU5A6XW.js → railroadDiagram-Y72NRBL7-DVnmXs8w.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-_0Pq4QdV.js → requirementDiagram-EQYSW5RA-BEoMdlzR.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-c9h-7uf8.js → sankeyDiagram-SY24JHO5-D7Tn2Gbs.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-B9-S4Rc9.js → sequenceDiagram-BLEHNHQU-CyjUDBaQ.js} +1 -1
- package/dist/ui/dist/assets/{src-BZ3kG7lO.js → src-BiC9XNcI.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-py1smSen.js → stateDiagram-v2-A6IESKWZ-CMp1VCO9.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-C1E90aH4.js → swimlanes-B3ENRKFQ-KEciLd6r.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-DNe1To6B.js → timeline-definition-JXYKGLGB-C8YQtv7M.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-D6MG89i6.js → treeView-D4JQ5SDB-DgAzPaOO.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-CZpt7Mlh.js → treemap-SAJKECNS-eCRqZMYc.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-CKsv-03M.js → usecaseDiagram-WPOLBNNX-Dirryw9G.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-DGJ1qlyJ.js → vennDiagram-Q3Y3IDCW-BjxQbAUx.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-ClOzBemC.js → wardley-7MLQ67FV-CqHX5RUz.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-C68ahPyF.js → wardleyDiagram-IVAIDKWA-Ds1HoEUY.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-DWDamCoH.js → xychartDiagram-4URNHDU4-kodzDb12.js} +1 -1
- package/dist/ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +5 -1
- package/dist/ui/dist/assets/channel-B7CT_oIp.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU--HPNoVQJ.js +0 -1
- package/dist/ui/dist/assets/index-B8TYcxF8.css +0 -1
- package/dist/ui/dist/assets/index-CYKbk4QS.js +0 -134
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-I98Q8AqP.js +0 -8
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { isRepoPath, isSha, tipStartOf } from "./diff.js";
|
|
4
|
+
import { GitError, pathsChanged } from "./git.js";
|
|
5
|
+
import { isFields } from "./plan.js";
|
|
6
|
+
/** lcov input that cannot be read, with the 1-based line it fails on. */
|
|
7
|
+
export class LcovError extends Error {
|
|
8
|
+
name = "LcovError";
|
|
9
|
+
line;
|
|
10
|
+
constructor(line, reason) {
|
|
11
|
+
super(`lcov line ${line}: ${reason}`);
|
|
12
|
+
this.line = line;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const RECORD = /^([A-Z][A-Z0-9_]*):(.*)$/;
|
|
16
|
+
const DA = /^([1-9][0-9]*),([0-9]+)(?:,.*)?$/;
|
|
17
|
+
const BRDA = /^([1-9][0-9]*),([^,]+),(.+),(-|[0-9]+)$/;
|
|
18
|
+
/**
|
|
19
|
+
* Reads lcov's tracefile format: TN names the test of the records after it, SF opens a file, DA
|
|
20
|
+
* and BRDA give line and branch hits, end_of_record closes it. Other records are skipped.
|
|
21
|
+
*/
|
|
22
|
+
export function parseLcov(text) {
|
|
23
|
+
const records = [];
|
|
24
|
+
let test = null;
|
|
25
|
+
let open = null;
|
|
26
|
+
const close = () => {
|
|
27
|
+
if (open !== null)
|
|
28
|
+
records.push(open);
|
|
29
|
+
open = null;
|
|
30
|
+
};
|
|
31
|
+
const rows = text.split("\n");
|
|
32
|
+
for (const [i, raw] of rows.entries()) {
|
|
33
|
+
const n = i + 1;
|
|
34
|
+
const row = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
35
|
+
if (row.trim() === "")
|
|
36
|
+
continue;
|
|
37
|
+
if (row === "end_of_record") {
|
|
38
|
+
close();
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const m = RECORD.exec(row);
|
|
42
|
+
if (m === null)
|
|
43
|
+
throw new LcovError(n, `"${row.slice(0, 60)}" is not an lcov record`);
|
|
44
|
+
const [, key = "", value = ""] = m;
|
|
45
|
+
if (key === "TN") {
|
|
46
|
+
test = value.trim() === "" ? null : value.trim();
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (key === "SF") {
|
|
50
|
+
close();
|
|
51
|
+
if (value === "")
|
|
52
|
+
throw new LcovError(n, "SF needs a path");
|
|
53
|
+
open = { path: value, test, lines: new Map(), branches: new Map() };
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (key !== "DA" && key !== "BRDA")
|
|
57
|
+
continue;
|
|
58
|
+
if (open === null)
|
|
59
|
+
throw new LcovError(n, `${key} comes before any SF`);
|
|
60
|
+
if (key === "DA") {
|
|
61
|
+
const da = DA.exec(value);
|
|
62
|
+
if (da === null)
|
|
63
|
+
throw new LcovError(n, `DA needs <line>,<hits>, got "${value}"`);
|
|
64
|
+
const line = Number(da[1]);
|
|
65
|
+
open.lines.set(line, (open.lines.get(line) ?? 0) + Number(da[2]));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const br = BRDA.exec(value);
|
|
69
|
+
if (br === null) {
|
|
70
|
+
throw new LcovError(n, `BRDA needs <line>,<block>,<branch>,<taken>, got "${value}"`);
|
|
71
|
+
}
|
|
72
|
+
const line = Number(br[1]);
|
|
73
|
+
const id = `${br[2]},${br[3]}`;
|
|
74
|
+
const taken = br[4] !== "-" && Number(br[4]) > 0;
|
|
75
|
+
const branches = open.branches.get(line) ?? new Map();
|
|
76
|
+
branches.set(id, (branches.get(id) ?? false) || taken);
|
|
77
|
+
open.branches.set(line, branches);
|
|
78
|
+
}
|
|
79
|
+
close();
|
|
80
|
+
if (records.length === 0)
|
|
81
|
+
throw new LcovError(rows.length, "no SF record; is this lcov?");
|
|
82
|
+
return records;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Where an lcov path is at the tip: absolute, or relative to cwd and failing that to the root,
|
|
86
|
+
* symlinks resolved, then relative to the root. Null outside the repo or for a file the tip lacks.
|
|
87
|
+
*/
|
|
88
|
+
export function tipPlacer(root, cwd, atTip) {
|
|
89
|
+
const real = (p) => {
|
|
90
|
+
try {
|
|
91
|
+
return realpathSync(p);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return p;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const tops = [root, real(root)];
|
|
98
|
+
return (raw) => {
|
|
99
|
+
const tried = isAbsolute(raw) ? [raw] : [resolve(cwd, raw), resolve(root, raw)];
|
|
100
|
+
for (const abs of tried.flatMap((p) => [p, real(p)])) {
|
|
101
|
+
for (const top of tops) {
|
|
102
|
+
const rel = relative(top, abs);
|
|
103
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
|
|
104
|
+
continue;
|
|
105
|
+
const path = Buffer.from(rel.split(sep).join("/"), "utf8").toString("latin1");
|
|
106
|
+
if (isRepoPath(path) && atTip.has(path))
|
|
107
|
+
return path;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Sums hits and joins branches over records of the same file; drops what `place` rejects. */
|
|
114
|
+
export function runOf(records, place) {
|
|
115
|
+
const lines = new Map();
|
|
116
|
+
const branches = new Map();
|
|
117
|
+
const tests = new Map();
|
|
118
|
+
for (const record of records) {
|
|
119
|
+
const path = place(record.path);
|
|
120
|
+
if (path === null)
|
|
121
|
+
continue;
|
|
122
|
+
const hits = lines.get(path) ?? new Map();
|
|
123
|
+
for (const [line, n] of record.lines)
|
|
124
|
+
hits.set(line, (hits.get(line) ?? 0) + n);
|
|
125
|
+
lines.set(path, hits);
|
|
126
|
+
const joined = branches.get(path) ?? new Map();
|
|
127
|
+
for (const [line, ids] of record.branches) {
|
|
128
|
+
const on = joined.get(line) ?? new Map();
|
|
129
|
+
for (const [id, taken] of ids)
|
|
130
|
+
on.set(id, (on.get(id) ?? false) || taken);
|
|
131
|
+
joined.set(line, on);
|
|
132
|
+
}
|
|
133
|
+
branches.set(path, joined);
|
|
134
|
+
if (record.test === null)
|
|
135
|
+
continue;
|
|
136
|
+
const ran = tests.get(record.test) ?? new Map();
|
|
137
|
+
const set = ran.get(path) ?? new Set();
|
|
138
|
+
for (const [line, n] of record.lines)
|
|
139
|
+
if (n > 0)
|
|
140
|
+
set.add(line);
|
|
141
|
+
ran.set(path, set);
|
|
142
|
+
tests.set(record.test, ran);
|
|
143
|
+
}
|
|
144
|
+
const files = new Map();
|
|
145
|
+
for (const [path, hits] of lines) {
|
|
146
|
+
const counted = new Map();
|
|
147
|
+
for (const [line, ids] of branches.get(path) ?? []) {
|
|
148
|
+
counted.set(line, [[...ids.values()].filter(Boolean).length, ids.size]);
|
|
149
|
+
}
|
|
150
|
+
files.set(path, { lines: sorted(hits), branches: sorted(counted) });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
files,
|
|
154
|
+
tests: new Map([...tests].map(([id, ran]) => [
|
|
155
|
+
id,
|
|
156
|
+
new Map([...ran].map(([path, set]) => [path, [...set].sort((a, b) => a - b)])),
|
|
157
|
+
])),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function sorted(map) {
|
|
161
|
+
return new Map([...map].sort(([a], [b]) => a - b));
|
|
162
|
+
}
|
|
163
|
+
/** The lines of each file with at least one hit. */
|
|
164
|
+
function hitLines(files) {
|
|
165
|
+
return new Map([...files].map(([path, f]) => [path, [...f.lines].flatMap(([l, n]) => (n > 0 ? [l] : []))]));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Per-test runs only add: a line keeps the larger count, so they never inflate an aggregate run
|
|
169
|
+
* of the same suite, and running the same test twice changes nothing.
|
|
170
|
+
*/
|
|
171
|
+
function mergeMax(a, b) {
|
|
172
|
+
const out = new Map(a);
|
|
173
|
+
for (const [path, f] of b) {
|
|
174
|
+
const had = out.get(path);
|
|
175
|
+
if (had === undefined) {
|
|
176
|
+
out.set(path, f);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const lines = new Map(had.lines);
|
|
180
|
+
for (const [l, n] of f.lines)
|
|
181
|
+
lines.set(l, Math.max(n, lines.get(l) ?? 0));
|
|
182
|
+
const branches = new Map(had.branches);
|
|
183
|
+
for (const [l, [taken, total]] of f.branches) {
|
|
184
|
+
const [t, n] = branches.get(l) ?? [0, 0];
|
|
185
|
+
branches.set(l, [Math.max(taken, t), Math.max(total, n)]);
|
|
186
|
+
}
|
|
187
|
+
out.set(path, { lines: sorted(lines), branches: sorted(branches) });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The coverage after this run. A plain run replaces everything, its TN blocks becoming its tests.
|
|
193
|
+
* A named test's run adds to what is already there for the same tip, and starts over otherwise.
|
|
194
|
+
*/
|
|
195
|
+
export function recordRun(prior, run, at) {
|
|
196
|
+
const { tip, takenAt, test } = at;
|
|
197
|
+
if (test === undefined) {
|
|
198
|
+
const tests = new Map([...run.tests].map(([id, lines]) => [id, { lines }]));
|
|
199
|
+
return { tip, takenAt, files: run.files, tests };
|
|
200
|
+
}
|
|
201
|
+
const base = prior !== null && prior.tip === tip ? prior : null;
|
|
202
|
+
const tests = new Map(base?.tests ?? []);
|
|
203
|
+
const entry = {
|
|
204
|
+
...(test.at === undefined ? {} : { at: test.at }),
|
|
205
|
+
lines: hitLines(run.files),
|
|
206
|
+
};
|
|
207
|
+
tests.set(test.id, entry);
|
|
208
|
+
const files = base === null ? run.files : mergeMax(base.files, run.files);
|
|
209
|
+
return { tip, takenAt, files, tests };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Per file, the branch's added rows that lcov calls executable, and how many ran. Every added
|
|
213
|
+
* row is a tip line, which is where the run was taken. Files without such rows are left out.
|
|
214
|
+
*/
|
|
215
|
+
export function changedLines(units, files) {
|
|
216
|
+
const out = new Map();
|
|
217
|
+
for (const unit of units.values()) {
|
|
218
|
+
if (unit.kind !== "hunk")
|
|
219
|
+
continue;
|
|
220
|
+
const file = files.get(unit.path);
|
|
221
|
+
if (file === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
let line = tipStartOf(unit, units);
|
|
224
|
+
let { covered, executable } = out.get(unit.path) ?? { covered: 0, executable: 0 };
|
|
225
|
+
for (const row of unit.body) {
|
|
226
|
+
const hits = row.op === "+" ? file.lines.get(line) : undefined;
|
|
227
|
+
if (hits !== undefined) {
|
|
228
|
+
executable += 1;
|
|
229
|
+
if (hits > 0)
|
|
230
|
+
covered += 1;
|
|
231
|
+
}
|
|
232
|
+
if (row.op !== "-")
|
|
233
|
+
line += 1;
|
|
234
|
+
}
|
|
235
|
+
if (executable > 0)
|
|
236
|
+
out.set(unit.path, { covered, executable });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
export function totalOf(tallies) {
|
|
241
|
+
let covered = 0;
|
|
242
|
+
let executable = 0;
|
|
243
|
+
for (const t of tallies) {
|
|
244
|
+
covered += t.covered;
|
|
245
|
+
executable += t.executable;
|
|
246
|
+
}
|
|
247
|
+
return { covered, executable };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Covered files whose content at `tip` differs from where the run was taken, in file order.
|
|
251
|
+
* When the run's commit is gone from the repo, every file is stale.
|
|
252
|
+
*/
|
|
253
|
+
export async function staleFiles(repo, coverage, tip) {
|
|
254
|
+
if (coverage.tip === tip)
|
|
255
|
+
return [];
|
|
256
|
+
let changed;
|
|
257
|
+
try {
|
|
258
|
+
changed = await pathsChanged(repo, coverage.tip, tip);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (!(error instanceof GitError))
|
|
262
|
+
throw error;
|
|
263
|
+
changed = new Set(coverage.files.keys());
|
|
264
|
+
}
|
|
265
|
+
return [...coverage.files.keys()].filter((p) => changed.has(p));
|
|
266
|
+
}
|
|
267
|
+
/** The files whose coverage still describes the tip. */
|
|
268
|
+
export function freshFiles(coverage, stale) {
|
|
269
|
+
const gone = new Set(stale);
|
|
270
|
+
return new Map([...coverage.files].filter(([path]) => !gone.has(path)));
|
|
271
|
+
}
|
|
272
|
+
export function serializeCoverage(c) {
|
|
273
|
+
const files = Object.fromEntries([...c.files].map(([path, f]) => [
|
|
274
|
+
path,
|
|
275
|
+
{
|
|
276
|
+
lines: Object.fromEntries(f.lines),
|
|
277
|
+
...(f.branches.size === 0 ? {} : { branches: Object.fromEntries(f.branches) }),
|
|
278
|
+
},
|
|
279
|
+
]));
|
|
280
|
+
const tests = Object.fromEntries([...c.tests].map(([id, t]) => [
|
|
281
|
+
id,
|
|
282
|
+
{
|
|
283
|
+
...(t.at === undefined ? {} : { at: t.at }),
|
|
284
|
+
lines: Object.fromEntries([...t.lines].map(([p, ls]) => [p, [...ls]])),
|
|
285
|
+
},
|
|
286
|
+
]));
|
|
287
|
+
return { tip: c.tip, takenAt: c.takenAt, files, ...(c.tests.size === 0 ? {} : { tests }) };
|
|
288
|
+
}
|
|
289
|
+
const LINE_KEY = /^[1-9][0-9]*$/;
|
|
290
|
+
function isCount(v) {
|
|
291
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
292
|
+
}
|
|
293
|
+
function isLine(v) {
|
|
294
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
295
|
+
}
|
|
296
|
+
function entries(v) {
|
|
297
|
+
return isFields(v) ? Object.entries(v) : null;
|
|
298
|
+
}
|
|
299
|
+
function parseLoc(v) {
|
|
300
|
+
if (!isFields(v))
|
|
301
|
+
return null;
|
|
302
|
+
const { path, start, end } = v;
|
|
303
|
+
if (typeof path !== "string" || !isRepoPath(path) || !isLine(start) || !isLine(end))
|
|
304
|
+
return null;
|
|
305
|
+
return end < start ? null : { path, start, end };
|
|
306
|
+
}
|
|
307
|
+
/** Like parseLoc, for argv: "path:line" or "path:start-end", the path placed like lcov's. */
|
|
308
|
+
export function parseLocArg(raw, place) {
|
|
309
|
+
const m = /^(.+):([1-9][0-9]*)(?:-([1-9][0-9]*))?$/.exec(raw);
|
|
310
|
+
if (m === null)
|
|
311
|
+
return `--at ${raw} is not <path>:<line> or <path>:<start>-<end>`;
|
|
312
|
+
const path = place(m[1] ?? "");
|
|
313
|
+
if (path === null)
|
|
314
|
+
return `--at ${raw} names no file of the branch tip`;
|
|
315
|
+
const start = Number(m[2]);
|
|
316
|
+
const end = m[3] === undefined ? start : Number(m[3]);
|
|
317
|
+
return end < start ? `--at ${raw} runs backwards` : { path, start, end };
|
|
318
|
+
}
|
|
319
|
+
function parseFile(v) {
|
|
320
|
+
if (!isFields(v))
|
|
321
|
+
return "a file is not an object";
|
|
322
|
+
const lines = entries(v.lines);
|
|
323
|
+
if (lines === null)
|
|
324
|
+
return "lines is not an object";
|
|
325
|
+
const hits = new Map();
|
|
326
|
+
for (const [key, n] of lines) {
|
|
327
|
+
if (!LINE_KEY.test(key) || !isCount(n))
|
|
328
|
+
return `line ${key} has no hit count`;
|
|
329
|
+
hits.set(Number(key), n);
|
|
330
|
+
}
|
|
331
|
+
const branchEntries = entries(v.branches ?? {});
|
|
332
|
+
if (branchEntries === null)
|
|
333
|
+
return "branches is not an object";
|
|
334
|
+
const branches = new Map();
|
|
335
|
+
for (const [key, pair] of branchEntries) {
|
|
336
|
+
if (!LINE_KEY.test(key) || !Array.isArray(pair) || pair.length !== 2) {
|
|
337
|
+
return `branches of line ${key} are not [taken, total]`;
|
|
338
|
+
}
|
|
339
|
+
const [taken, total] = pair;
|
|
340
|
+
if (!isCount(taken) || !isCount(total) || taken > total) {
|
|
341
|
+
return `branches of line ${key} are not [taken, total]`;
|
|
342
|
+
}
|
|
343
|
+
branches.set(Number(key), [taken, total]);
|
|
344
|
+
}
|
|
345
|
+
return { lines: sorted(hits), branches: sorted(branches) };
|
|
346
|
+
}
|
|
347
|
+
function parseTest(v) {
|
|
348
|
+
if (!isFields(v))
|
|
349
|
+
return "a test is not an object";
|
|
350
|
+
const at = v.at === undefined ? undefined : parseLoc(v.at);
|
|
351
|
+
if (at === null)
|
|
352
|
+
return "at is not a { path, start, end }";
|
|
353
|
+
const lineEntries = entries(v.lines);
|
|
354
|
+
if (lineEntries === null)
|
|
355
|
+
return "lines is not an object";
|
|
356
|
+
const lines = new Map();
|
|
357
|
+
for (const [path, list] of lineEntries) {
|
|
358
|
+
if (!isRepoPath(path) || !Array.isArray(list) || !list.every(isLine)) {
|
|
359
|
+
return `lines of ${path} are not line numbers`;
|
|
360
|
+
}
|
|
361
|
+
lines.set(path, list);
|
|
362
|
+
}
|
|
363
|
+
return { ...(at === undefined ? {} : { at }), lines };
|
|
364
|
+
}
|
|
365
|
+
/** The one place coverage.json becomes a Coverage. */
|
|
366
|
+
export function parseCoverage(v) {
|
|
367
|
+
if (!isFields(v))
|
|
368
|
+
return "not an object";
|
|
369
|
+
const { tip, takenAt } = v;
|
|
370
|
+
if (typeof tip !== "string" || !isSha(tip))
|
|
371
|
+
return "tip is not an object id";
|
|
372
|
+
if (typeof takenAt !== "string")
|
|
373
|
+
return "takenAt is not a time";
|
|
374
|
+
const fileEntries = entries(v.files);
|
|
375
|
+
if (fileEntries === null)
|
|
376
|
+
return "files is not an object";
|
|
377
|
+
const files = new Map();
|
|
378
|
+
for (const [path, raw] of fileEntries) {
|
|
379
|
+
if (!isRepoPath(path))
|
|
380
|
+
return "a file has no path";
|
|
381
|
+
const file = parseFile(raw);
|
|
382
|
+
if (typeof file === "string")
|
|
383
|
+
return `${path}: ${file}`;
|
|
384
|
+
files.set(path, file);
|
|
385
|
+
}
|
|
386
|
+
const testEntries = entries(v.tests ?? {});
|
|
387
|
+
if (testEntries === null)
|
|
388
|
+
return "tests is not an object";
|
|
389
|
+
const tests = new Map();
|
|
390
|
+
for (const [id, raw] of testEntries) {
|
|
391
|
+
const test = parseTest(raw);
|
|
392
|
+
if (typeof test === "string")
|
|
393
|
+
return `test ${id}: ${test}`;
|
|
394
|
+
tests.set(id, test);
|
|
395
|
+
}
|
|
396
|
+
return { tip, takenAt, files, tests };
|
|
397
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { splitLines, tipStartOf } from "./diff.js";
|
|
3
|
+
import { blobsAt, grepTip } from "./git.js";
|
|
4
|
+
export const MAX_CANDIDATES = 10;
|
|
5
|
+
const PREVIEW_LINES = 8;
|
|
6
|
+
/** An ASCII identifier as JS, Python, Go, Rust, JVM languages, Ruby and C spell one. */
|
|
7
|
+
export function isIdentifier(s) {
|
|
8
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(s);
|
|
9
|
+
}
|
|
10
|
+
const JS_MODS = "((static|async|public|private|protected|readonly|override|get|set)\\s+)*";
|
|
11
|
+
const RUST_VIS = "(pub([(][^)]*[)])?\\s+)?";
|
|
12
|
+
const JAVA_MODS = "((public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\\s+)*";
|
|
13
|
+
const JVM_CLASS_MODS = "((public|private|protected|static|final|abstract|sealed|data|open|internal|inner|enum|annotation)\\s+)*";
|
|
14
|
+
const KOTLIN_MODS = "((public|private|protected|internal|override|open|abstract|suspend|inline|operator|infix|tailrec|external|actual|expect)\\s+)*";
|
|
15
|
+
const STATEMENT = /^\s*(return|new|throw|else|case|yield|await|typedef|delete|goto)\b/;
|
|
16
|
+
const LANGUAGES = [
|
|
17
|
+
{
|
|
18
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
|
19
|
+
patterns: [
|
|
20
|
+
{
|
|
21
|
+
kind: "function",
|
|
22
|
+
source: "^\\s*(export\\s+)?(default\\s+)?(async\\s+)?function\\s*[*]?\\s*NAME\\s*[(<]",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: "const",
|
|
26
|
+
source: "^\\s*(export\\s+)?(declare\\s+)?(const|let|var)\\s+NAME\\s*(:[^=]*)?=",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
kind: "class",
|
|
30
|
+
source: "^\\s*(export\\s+)?(default\\s+)?(declare\\s+)?(abstract\\s+)?class\\s+NAMEEND",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: "type",
|
|
34
|
+
source: "^\\s*(export\\s+)?(declare\\s+)?(interface|type|(const\\s+)?enum)\\s+NAMEEND",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
kind: "method",
|
|
38
|
+
source: `^\\s+${JS_MODS}[*]?NAME\\s*(<[^>]*>)?[(].*[)]\\s*(:[^={;]+)?[{]\\s*$`,
|
|
39
|
+
reject: /^\s*(if|for|while|switch|catch|return)\b/,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
extensions: [".py", ".pyi"],
|
|
45
|
+
patterns: [
|
|
46
|
+
{ kind: "function", source: "^(async\\s+)?def\\s+NAME\\s*[(]" },
|
|
47
|
+
{ kind: "method", source: "^\\s+(async\\s+)?def\\s+NAME\\s*[(]" },
|
|
48
|
+
{ kind: "class", source: "^\\s*class\\s+NAME\\s*[(:]" },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
extensions: [".go"],
|
|
53
|
+
patterns: [
|
|
54
|
+
{ kind: "function", source: "^func\\s+NAME\\s*[[(]" },
|
|
55
|
+
{ kind: "method", source: "^func\\s*[(][^)]*[)]\\s*NAME\\s*[[(]" },
|
|
56
|
+
{ kind: "type", source: "^\\s*type\\s+NAMEEND" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
extensions: [".rs"],
|
|
61
|
+
patterns: [
|
|
62
|
+
{
|
|
63
|
+
kind: "function",
|
|
64
|
+
source: `^${RUST_VIS}((const|async|unsafe|extern)\\s+("[^"]*"\\s+)?)*fn\\s+NAME\\s*[<(]`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: "method",
|
|
68
|
+
source: `^\\s+${RUST_VIS}((const|async|unsafe|extern)\\s+("[^"]*"\\s+)?)*fn\\s+NAME\\s*[<(]`,
|
|
69
|
+
},
|
|
70
|
+
{ kind: "class", source: `^\\s*${RUST_VIS}struct\\s+NAMEEND` },
|
|
71
|
+
{ kind: "type", source: `^\\s*${RUST_VIS}(enum|trait|type)\\s+NAMEEND` },
|
|
72
|
+
{ kind: "other", source: "^\\s*impl(<[^>]*>)?\\s+([A-Za-z0-9_:<>, ]+\\s+for\\s+)?NAMEEND" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
extensions: [".java", ".kt", ".kts"],
|
|
77
|
+
patterns: [
|
|
78
|
+
{ kind: "class", source: `^\\s*${JVM_CLASS_MODS}(class|object|record)\\s+NAMEEND` },
|
|
79
|
+
{ kind: "type", source: `^\\s*${JVM_CLASS_MODS}(interface|enum|@interface)\\s+NAMEEND` },
|
|
80
|
+
{
|
|
81
|
+
kind: "function",
|
|
82
|
+
source: `^\\s*${KOTLIN_MODS}fun\\s+(<[^>]*>\\s*)?([A-Za-z0-9_.<>]+[.])?NAME\\s*[(]`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: "method",
|
|
86
|
+
source: `^\\s*${JAVA_MODS}(<[^>]*>\\s+)?[A-Za-z_][A-Za-z0-9_<>,.?]*(\\[\\])*\\s+NAME\\s*[(][^;]*$`,
|
|
87
|
+
reject: STATEMENT,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
extensions: [".rb"],
|
|
93
|
+
patterns: [
|
|
94
|
+
{ kind: "method", source: "^\\s*def\\s+(self[.])?NAMEEND" },
|
|
95
|
+
{ kind: "class", source: "^\\s*class\\s+NAMEEND" },
|
|
96
|
+
{ kind: "other", source: "^\\s*module\\s+NAMEEND" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx"],
|
|
101
|
+
patterns: [
|
|
102
|
+
{
|
|
103
|
+
kind: "function",
|
|
104
|
+
source: "^[A-Za-z_][A-Za-z0-9_ *&<>,:~]*[ *&]([A-Za-z_][A-Za-z0-9_]*::)*NAME\\s*[(][^;]*$",
|
|
105
|
+
reject: STATEMENT,
|
|
106
|
+
},
|
|
107
|
+
{ kind: "function", source: "^NAME\\s*[(][^;]*$" },
|
|
108
|
+
{ kind: "class", source: "^\\s*(typedef\\s+)?(struct|class|union)\\s+NAME\\s*([{:]|$)" },
|
|
109
|
+
{ kind: "type", source: "^\\s*(typedef\\s+)?enum\\s+(class\\s+)?NAME\\s*([{:]|$)" },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const END = "([^A-Za-z0-9_$]|$)";
|
|
114
|
+
function spell(source, name) {
|
|
115
|
+
return source
|
|
116
|
+
.replaceAll("NAMEEND", `${name.replaceAll("$", "[$]")}${END}`)
|
|
117
|
+
.replaceAll("NAME", name.replaceAll("$", "[$]"));
|
|
118
|
+
}
|
|
119
|
+
function toEre(source) {
|
|
120
|
+
return source.replaceAll("\\s", "[[:space:]]");
|
|
121
|
+
}
|
|
122
|
+
function languageOf(path) {
|
|
123
|
+
const ext = posix.extname(path).toLowerCase();
|
|
124
|
+
return LANGUAGES.find((l) => l.extensions.includes(ext)) ?? null;
|
|
125
|
+
}
|
|
126
|
+
/** What the match defines by the patterns of its file's language, or null when it is no definition. */
|
|
127
|
+
function kindOf(match, name) {
|
|
128
|
+
const language = languageOf(match.path);
|
|
129
|
+
if (language === null)
|
|
130
|
+
return null;
|
|
131
|
+
const hit = language.patterns.find((p) => new RegExp(spell(p.source, name)).test(match.text) && !p.reject?.test(match.text));
|
|
132
|
+
return hit?.kind ?? null;
|
|
133
|
+
}
|
|
134
|
+
/** Same file as `from` first, then the same directory, then the rest, each in path and line order. */
|
|
135
|
+
export function rank(found, from) {
|
|
136
|
+
const tier = (path) => {
|
|
137
|
+
if (from === null)
|
|
138
|
+
return 2;
|
|
139
|
+
if (path === from)
|
|
140
|
+
return 0;
|
|
141
|
+
return posix.dirname(path) === posix.dirname(from) ? 1 : 2;
|
|
142
|
+
};
|
|
143
|
+
return found
|
|
144
|
+
.map((f, i) => ({ f, i, t: tier(f.path) }))
|
|
145
|
+
.sort((a, b) => a.t - b.t || a.i - b.i)
|
|
146
|
+
.map(({ f }) => f);
|
|
147
|
+
}
|
|
148
|
+
const NOWHERE = { unit: null, row: null, commit: null };
|
|
149
|
+
/**
|
|
150
|
+
* The hunk row that holds tip line `line` of `path`, and the commit that writes it: the owner of a
|
|
151
|
+
* change row, or the first commit taking the unit for a context row.
|
|
152
|
+
*/
|
|
153
|
+
export function place(units, commits, path, line) {
|
|
154
|
+
for (const unit of units.values()) {
|
|
155
|
+
if (unit.kind !== "hunk" || unit.path !== path)
|
|
156
|
+
continue;
|
|
157
|
+
const row = rowAt(unit, tipStartOf(unit, units), line);
|
|
158
|
+
if (row === null)
|
|
159
|
+
continue;
|
|
160
|
+
const body = unit.body[row];
|
|
161
|
+
const change = body !== undefined && body.op !== " " ? body.change : null;
|
|
162
|
+
const takes = (c) => c.refs.some((r) => r.unit === unit.id &&
|
|
163
|
+
(change === null || r.select.kind === "rest" || r.select.changes.includes(change)));
|
|
164
|
+
const k = commits.findIndex(takes);
|
|
165
|
+
return { unit: unit.id, row, commit: k === -1 ? null : k };
|
|
166
|
+
}
|
|
167
|
+
return NOWHERE;
|
|
168
|
+
}
|
|
169
|
+
function rowAt(hunk, tipStart, line) {
|
|
170
|
+
let tipLine = tipStart;
|
|
171
|
+
for (const [row, body] of hunk.body.entries()) {
|
|
172
|
+
if (body.op === "-")
|
|
173
|
+
continue;
|
|
174
|
+
if (tipLine === line)
|
|
175
|
+
return row;
|
|
176
|
+
tipLine += 1;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function stripEol(line) {
|
|
181
|
+
return line.replace(/\r?\n$/, "");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Where names are defined at one branch tip and where each lands in the plan. Built per hosted
|
|
185
|
+
* version, so a swap starts a fresh one; matches are cached per name and file lines per path.
|
|
186
|
+
*/
|
|
187
|
+
export function definitionIndex(repo, tip, units, commits) {
|
|
188
|
+
const foundByName = new Map();
|
|
189
|
+
const linesByPath = new Map();
|
|
190
|
+
const find = (name) => {
|
|
191
|
+
const cached = foundByName.get(name);
|
|
192
|
+
if (cached !== undefined)
|
|
193
|
+
return cached;
|
|
194
|
+
const patterns = [
|
|
195
|
+
...new Set(LANGUAGES.flatMap((l) => l.patterns.map((p) => toEre(spell(p.source, name))))),
|
|
196
|
+
];
|
|
197
|
+
const pathspecs = LANGUAGES.flatMap((l) => l.extensions.map((e) => `*${e}`));
|
|
198
|
+
const found = grepTip(repo, tip, patterns, pathspecs).then((matches) => matches.flatMap((m) => {
|
|
199
|
+
const kind = kindOf(m, name);
|
|
200
|
+
return kind === null ? [] : [{ path: m.path, newLine: m.line, kind }];
|
|
201
|
+
}));
|
|
202
|
+
foundByName.set(name, found);
|
|
203
|
+
// A failed grep is not an answer; the next request tries again.
|
|
204
|
+
found.catch(() => foundByName.delete(name));
|
|
205
|
+
return found;
|
|
206
|
+
};
|
|
207
|
+
const load = async (paths) => {
|
|
208
|
+
const missing = [...new Set(paths)].filter((p) => !linesByPath.has(p));
|
|
209
|
+
if (missing.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
for (const [path, bytes] of await blobsAt(repo, tip, missing)) {
|
|
212
|
+
linesByPath.set(path, splitLines(bytes).map(stripEol));
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
lookup: async (name, from) => {
|
|
217
|
+
const ranked = rank(await find(name), from).slice(0, MAX_CANDIDATES);
|
|
218
|
+
await load(ranked.map((f) => f.path));
|
|
219
|
+
return ranked.map((f) => ({
|
|
220
|
+
...f,
|
|
221
|
+
...place(units, commits, f.path, f.newLine),
|
|
222
|
+
preview: (linesByPath.get(f.path) ?? []).slice(f.newLine - 1, f.newLine - 1 + PREVIEW_LINES),
|
|
223
|
+
}));
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|