pi-squeeze 0.1.0 → 0.2.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.
@@ -1,10 +1,28 @@
1
1
  /**
2
- * `gradle` / `./gradlew` output: every `> Task :x UP-TO-DATE` line (and NO-SOURCE, FROM-CACHE,
3
- * SKIPPED, plain executed ones) becomes one counting line; failed tasks, compiler errors and
4
- * warnings, test failures and the `BUILD` result stay. The daemon, configuration-cache and
5
- * "Run with --scan" hints go. Kotlin `w:` / `e:` lines lose their shared directory prefix.
2
+ * `gradle` / `./gradlew` output:
3
+ *
4
+ * * every `> Task :x UP-TO-DATE` line (and NO-SOURCE, FROM-CACHE, SKIPPED, plain executed
5
+ * ones) becomes one counting line; failed tasks, errors, test failures and the `BUILD`
6
+ * result stay;
7
+ * * daemon, configuration-cache, AGP-option, JVM and "Run with --scan" chatter goes, as do
8
+ * the wrappers around a failure (`A failure occurred while executing …`), the lint
9
+ * baseline advice and dependencyInsight's attribute tables;
10
+ * * repeated warnings and `--warning-mode all` deprecations are said once, with a count,
11
+ * without the upgrade-guide links; the failure summary does not repeat what was printed
12
+ * above it;
13
+ * * Kotlin / KSP warnings, detekt and ktlint findings and Android Lint issues are grouped by
14
+ * message, each location listed once (see `renderDiags`); errors stay one per line;
15
+ * * R8 "Missing class" lines become one list of class names, "Duplicate class" lines one
16
+ * list per pair of modules; "Searched in the following locations" lists become a count;
17
+ * * `tasks` and `projects` reports become names per group (see gradlereports.ts);
18
+ * * reported files lose the directory they share; framework stack frames fold.
6
19
  */
7
20
  import { capLines, cutLine, DEFAULT_LIMITS, type Limits, type Squeezed } from "./elide.ts";
21
+ import { squeezeProjects, squeezeTaskReport } from "./gradlereports.ts";
22
+ import { isJvmNoise } from "./jvm.ts";
23
+ import { type Diag, renderDiags } from "./lint.ts";
24
+ import { foldFrames } from "./logs.ts";
25
+ import { compactTree } from "./tree.ts";
8
26
 
9
27
  const TASK = /^> Task (\S+)(?: ([A-Z-]+))?$/;
10
28
  const NOISE = [
@@ -12,6 +30,7 @@ const NOISE = [
12
30
  /^Daemon will be stopped/,
13
31
  /^Calculating task graph/,
14
32
  /^(Reusing|Configuration) cache/,
33
+ /^Reusing configuration cache\.$/,
15
34
  /^Configuration cache entry/,
16
35
  /^Isolated Projects is an incubating/,
17
36
  /^Consider enabling configuration cache/,
@@ -21,53 +40,230 @@ const NOISE = [
21
40
  /^Deprecated Gradle features were used/,
22
41
  /^You can use '--warning-mode all'/,
23
42
  /^For more on this, please refer to/,
43
+ /^\[Incubating\] Problems report is available/,
44
+ /^Type-safe project accessors is an incubating/,
45
+ // AGP's explanation under each `WARNING: The option setting …` line.
46
+ /^The current default is '/,
47
+ /^It will be removed in version [\d.]+ of the Android Gradle plugin/,
48
+ /^Add android\.sync\.suppress\w+=/,
49
+ /^Download https?:\/\//,
50
+ /^={20,}$/,
51
+ /^-{5,}$/,
52
+ // `gradle dependencyUpdates`: the URL under each dependency.
53
+ /^\s+https?:\/\/\S+$/,
54
+ // Wrappers around the actual failure.
55
+ /^\s*> A failure occurred while executing [\w.$]+$/,
56
+ /^\s*> There was a failure while executing work items$/,
57
+ /^\s*Go to the documentation to learn how to <a href=/,
58
+ // dependencyInsight: attribute tables and the legend.
59
+ /^\s+\|.*\|\s*$/,
60
+ /^\(\*\) - Indicates repeated occurrences/,
61
+ /^\([cn]\) - (?:A dependency constraint|A dependency or dependency configuration)/,
62
+ /^A web-based, searchable dependency report is available/,
63
+ // `gradle projects`.
64
+ /^To see a list of the tasks of a project, run/,
65
+ /^For example, try running gradlew/,
66
+ /^Project locations:$/,
67
+ // javac's advice to rerun with -Xlint.
68
+ /^Note: (?:Recompile with -Xlint|Some input files use)/,
24
69
  ];
25
- const KOTLIN = /^([we]): (?:file:\/\/)?(\/\S+?):(\d+):(\d+)(.*)$/;
70
+ /** Blocks of advice, dropped from the first line to the last. */
71
+ const ADVICE = [{ start: /^\s*Fix the issues identified by lint, or create a baseline/, end: /^\s*For more details, see https:\/\/developer\.android\.com\/studio\/write\/lint/ }];
72
+ /** Lines said once are enough (their indented continuation goes with a repeat too). */
73
+ const ONCE = /^(w: |e: |WARNING: |Lint found \d+|The full lint text report is located at:)/;
74
+ /** `--warning-mode all` deprecations, said once and without the guide link. */
75
+ const DEPRECATION = /scheduled to be removed in Gradle \d+|will fail with an error in Gradle \d+|has been deprecated\./;
76
+ const GUIDE = /\s*(?:Consult the upgrading guide for further information|For more information, please refer to|Please refer to|See)\s*:?\s*https?:\/\/\S+.*$/;
77
+
78
+ /** Findings that group by message: Kotlin / KSP warnings, detekt / ktlint, Android Lint. */
79
+ const KOTLIN_W = /^w: (?:\[ksp\] )?(?:file:\/\/)?(\/\S+?):(\d+)(?::(\d+))?:? (.*)$/;
80
+ const LINT_ISSUE = /^(\/\S+?):(\d+): (Error|Warning|Information|Hint|Fatal): (.*) \[(\w+)\]$/;
81
+ const CHECKER = /^(\/\S+?):(\d+):(\d+): (.*?)(?: \[([\w:-]+)\])?$/;
82
+ /** R8 and AGP's duplicate-class check. */
83
+ const R8_MISSING = /^(?:ERROR: )?(?:R8: )?Missing class (\S+)(?: \(referenced from: .*\))?$/;
84
+ const DUPLICATE = /^\s*(?:> )?Duplicate class (\S+) found in modules? (.+)$/;
85
+
86
+ /** Every absolute file path followed by a line number, `:12` or Maven's `:[12,5]` (not a URL's host and port). */
87
+ const PATH_AT = /(?<![\w/:.])(?:file:\/\/)?(\/[^\s:'"()[\]]+\.[A-Za-z]\w*):(?:\d|\[\d)/g;
88
+
89
+ type Group = { tool: string; diags: Diag[] };
90
+ type Listed = { label: string; names: string[] };
26
91
 
27
92
  export function compactGradle(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
28
93
  const lines = output.split("\n");
29
94
  while (lines.length && lines.at(-1) === "") lines.pop();
30
95
  if (!lines.some((l) => TASK.test(l)) || !lines.some((l) => /^BUILD (SUCCESSFUL|FAILED)/.test(l))) return undefined;
31
96
 
32
- const states = new Map<string, number>();
33
- const ran: string[] = [];
34
- const kept: string[] = [];
35
- let tasks = 0;
36
- for (const l of lines) {
97
+ const s = scan(lines);
98
+ shortenPaths(s.kept, s.slots);
99
+ const width = limits.lineChars * 8;
100
+ let cut = s.cut;
101
+ const expanded = s.kept.flatMap((l, i) => {
102
+ const key = s.slots.get(i);
103
+ if (key === undefined) return [l];
104
+ const g = s.groups.get(key);
105
+ if (g) {
106
+ const r = renderDiags({ tool: g.tool, diags: g.diags, notes: [] }, { ...limits, total: Number.MAX_SAFE_INTEGER });
107
+ cut ||= r.truncated;
108
+ return r.text.split("\n");
109
+ }
110
+ const { label, names } = s.listed.get(key)!;
111
+ return [cutLine(`${label} (${names.length}): ${names.join(" ")}`, width)];
112
+ });
113
+ const tasksReport = squeezeTaskReport(expanded, width);
114
+ const projects = squeezeProjects(tasksReport.lines, width);
115
+ const { lines: folded, folded: didFold } = foldFrames(projects.lines);
116
+
117
+ // Blank lines only separate: never two in a row, never first. Lists may run to `width`.
118
+ const body: string[] = [];
119
+ for (const l of folded) {
120
+ const blank = l.trim() === "";
121
+ if (blank && (body.length === 0 || body.at(-1) === "")) continue;
122
+ body.push(blank ? "" : cutLine(l, LIST_LINE.test(l) ? width : limits.lineChars));
123
+ }
124
+ while (body.at(-1) === "") body.pop();
125
+
126
+ const parts = [...s.states].map(([k, v]) => `${v} ${k.toLowerCase()}`);
127
+ const head = `gradle: ${s.tasks} tasks (${parts.join(", ")})${s.ran.length ? ` ran: ${s.ran.slice(0, 12).join(" ")}${s.ran.length > 12 ? ` … +${s.ran.length - 12}` : ""}` : ""}`;
128
+ const capped = capLines([head, ...body], limits.total);
129
+ return { text: capped.text, truncated: capped.truncated || didFold || cut || tasksReport.cut || projects.cut };
130
+ }
131
+
132
+ /** `label (N): name name …`: one line of names, which may run long. */
133
+ const LIST_LINE = /^\S.* \(\d+\): /;
134
+
135
+ type Scan = {
136
+ kept: string[];
137
+ states: Map<string, number>;
138
+ ran: string[];
139
+ tasks: number;
140
+ groups: Map<string, Group>;
141
+ listed: Map<string, Listed>;
142
+ /** Lines where a group of findings or a list of names goes once all of it is known. */
143
+ slots: Map<number, string>;
144
+ /** Something was left out that the text does not show: R8 / duplicate-class references. */
145
+ cut: boolean;
146
+ };
147
+
148
+ /** Goes through the lines once: counts tasks, drops noise and repeats, collects findings. */
149
+ function scan(lines: string[]): Scan {
150
+ const s: Scan = { kept: [], states: new Map(), ran: [], tasks: 0, groups: new Map(), listed: new Map(), slots: new Map(), cut: false };
151
+ const { kept } = s;
152
+ /** Where each said-once line went, and how often it came. */
153
+ const said = new Map<string, { at: number; n: number }>();
154
+ /** Trimmed lines printed before the failure summary, which repeats them. */
155
+ const printed = new Set<string>();
156
+ let skip: "try" | "repeat" | "issue" | RegExp | undefined;
157
+ let summary = false;
158
+ let searched: { at: number; n: number; indent: string } | undefined;
159
+
160
+ const slot = (key: string) => s.slots.set(kept.push("") - 1, key);
161
+ const group = (tool: string, d: Diag) => {
162
+ const g = s.groups.get(tool) ?? { tool, diags: [] };
163
+ if (!s.groups.has(tool)) slot(tool);
164
+ if (!g.diags.some((x) => x.file === d.file && x.line === d.line && x.message === d.message)) g.diags.push(d);
165
+ s.groups.set(tool, g);
166
+ };
167
+ const list = (key: string, label: string, name: string) => {
168
+ const g = s.listed.get(key) ?? { label, names: [] };
169
+ if (!s.listed.has(key)) slot(key);
170
+ if (!g.names.includes(name)) g.names.push(name);
171
+ s.listed.set(key, g);
172
+ s.cut = true;
173
+ };
174
+ const endSearch = () => {
175
+ kept[searched!.at] = `${searched!.indent}Searched in ${searched!.n} locations`;
176
+ searched = undefined;
177
+ };
178
+
179
+ for (const raw of lines) {
180
+ const l = DEPRECATION.test(raw) ? raw.replace(GUIDE, "") : raw;
181
+ s.cut ||= l !== raw;
182
+ if (/^(?:\* What went wrong:|FAILURE: )/.test(l)) summary = true;
183
+ if (!summary) printed.add(l.trim());
37
184
  const t = TASK.exec(l);
38
185
  if (t) {
39
- tasks++;
186
+ s.tasks++;
40
187
  if (t[2] === "FAILED") kept.push(l);
41
- else if (t[2]) states.set(t[2], (states.get(t[2]) ?? 0) + 1);
42
- else (states.set("executed", (states.get("executed") ?? 0) + 1), ran.push(t[1]!));
188
+ else if (t[2]) s.states.set(t[2], (s.states.get(t[2]) ?? 0) + 1);
189
+ else (s.states.set("executed", (s.states.get("executed") ?? 0) + 1), s.ran.push(t[1]!));
190
+ skip = undefined;
43
191
  continue;
44
192
  }
45
- if (NOISE.some((n) => n.test(l))) continue;
46
- kept.push(l);
47
- }
48
-
49
- // Kotlin diagnostics: strip the directory every file shares.
50
- const paths = kept.map((l) => KOTLIN.exec(l)?.[2]).filter((p): p is string => !!p);
51
- if (paths.length > 1) {
52
- const dirs = paths.map((p) => p.slice(0, p.lastIndexOf("/") + 1));
53
- let prefix = dirs[0]!;
54
- for (const d of dirs) while (!d.startsWith(prefix)) prefix = prefix.slice(0, prefix.lastIndexOf("/", prefix.length - 2) + 1);
55
- if (prefix.length > 1)
56
- for (let i = 0; i < kept.length; i++) {
57
- const m = KOTLIN.exec(kept[i]!);
58
- if (m) kept[i] = `${m[1]}: …/${m[2]!.slice(prefix.length)}:${m[3]}:${m[4]}${m[5]}`;
193
+ if (searched) {
194
+ if (/^\s+- \S+$/.test(l)) {
195
+ searched.n++;
196
+ s.cut = true;
197
+ continue;
59
198
  }
60
- }
199
+ endSearch();
200
+ }
201
+ if (skip instanceof RegExp) {
202
+ if (skip.test(l)) skip = undefined;
203
+ continue;
204
+ }
205
+ if (skip) {
206
+ // `* Try:` runs to a blank line; a repeat's and an issue's continuation is indented.
207
+ const more = skip === "try" ? l.trim() !== "" : l === "" || /^\s/.test(l);
208
+ if (more) continue;
209
+ skip = undefined;
210
+ }
211
+ if (l === "* Try:") {
212
+ skip = "try";
213
+ continue;
214
+ }
215
+ const advice = ADVICE.find((a) => a.start.test(l));
216
+ if (advice) {
217
+ skip = advice.end;
218
+ continue;
219
+ }
220
+ if (NOISE.some((n) => n.test(l)) || isJvmNoise(l)) continue;
221
+ // The failure summary repeats lint's first issue, Kotlin's error and the like: once is enough.
222
+ const trimmed = l.trim();
223
+ if (summary && trimmed.length >= 4 && !trimmed.startsWith("* ") && printed.has(trimmed)) continue;
61
224
 
62
- // Blank lines only separate: never two in a row, never first.
63
- const body: string[] = [];
64
- for (const l of kept) {
65
- if (l === "" && (body.length === 0 || body.at(-1) === "")) continue;
66
- body.push(cutLine(l, limits.lineChars));
225
+ const kw = KOTLIN_W.exec(l);
226
+ const li = LINT_ISSUE.exec(l);
227
+ const ck = CHECKER.exec(l);
228
+ const r8 = R8_MISSING.exec(l);
229
+ const dup = DUPLICATE.exec(l);
230
+ if (kw) group("kotlin", { file: kw[1]!, line: kw[2]!, col: kw[3], severity: "warning", message: kw[4]! });
231
+ else if (li) {
232
+ group("android lint", { file: li[1]!, line: li[2]!, severity: li[3]!.toLowerCase(), rule: li[5], message: li[4]! });
233
+ skip = "issue";
234
+ s.cut = true;
235
+ } else if (ck) group("detekt / ktlint", { file: ck[1]!, line: ck[2]!, col: ck[3], severity: "", rule: ck[5], message: ck[4]! });
236
+ else if (r8) list("r8", "R8: missing classes", r8[1]!);
237
+ else if (dup) list(`dup ${dup[2]}`, `Duplicate classes in ${dup[2]}`, dup[1]!);
238
+ else if (ONCE.test(l) || DEPRECATION.test(l)) {
239
+ const seen = said.get(l);
240
+ if (seen) {
241
+ seen.n++;
242
+ skip = "repeat";
243
+ } else said.set(l, { at: kept.push(l) - 1, n: 1 });
244
+ } else {
245
+ const sl = /^(\s*)Searched in the following locations:$/.exec(l);
246
+ if (sl) searched = { at: kept.length, n: 0, indent: sl[1]! };
247
+ kept.push(l);
248
+ }
67
249
  }
68
- while (body.at(-1) === "") body.pop();
250
+ if (searched) endSearch();
251
+ for (const { at, n } of said.values()) if (n > 1) kept[at] += ` (×${n})`;
252
+ return s;
253
+ }
254
+
255
+ /** Writes the directory every reported file shares once (grouped findings do their own). */
256
+ export function shortenPaths(kept: string[], slots: Map<number, string> = new Map()) {
257
+ const paths = kept.flatMap((l, i) => (slots.has(i) ? [] : [...l.matchAll(PATH_AT)].map((m) => m[1]!)));
258
+ if (paths.length < 2) return;
259
+ let prefix = paths[0]!.slice(0, paths[0]!.lastIndexOf("/") + 1);
260
+ for (const p of paths) while (!p.startsWith(prefix)) prefix = prefix.slice(0, prefix.lastIndexOf("/", prefix.length - 2) + 1);
261
+ if (prefix.length > 1) for (let i = 0; i < kept.length; i++) kept[i] = kept[i]!.replaceAll(`file://${prefix}`, "…/").replaceAll(prefix, "…/");
262
+ }
69
263
 
70
- const parts = [...states].map(([k, v]) => `${v} ${k.toLowerCase()}`);
71
- const head = `gradle: ${tasks} tasks (${parts.join(", ")})${ran.length ? ` ran: ${ran.slice(0, 12).join(" ")}${ran.length > 12 ? ` … +${ran.length - 12}` : ""}` : ""}`;
72
- return capLines([head, ...body], limits.total);
264
+ /** `gradle dependencies`: the gradle clean-up, then the tree cut below its depth. */
265
+ export function compactGradleDeps(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
266
+ const cleaned = compactGradle(output, limits);
267
+ const tree = compactTree(cleaned?.text ?? output, limits);
268
+ return tree ?? cleaned;
73
269
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Gradle's own reports, which list far more than anyone reads:
3
+ *
4
+ * * `gradle tasks [--all]`: per group, the task names on one line; the descriptions go.
5
+ * * `gradle projects`: every project path (Gradle prints them twice, flat and as a tree)
6
+ * once, grouped by the first segment: `:api (60): analytics analytics:core …`.
7
+ */
8
+ import { cutLine } from "./elide.ts";
9
+
10
+ type Rewritten = { lines: string[]; cut: boolean };
11
+
12
+ const TASKS_HEAD = /^Tasks runnable from (?:root )?project\b/;
13
+
14
+ export function squeezeTaskReport(lines: string[], width: number): Rewritten {
15
+ const start = lines.findIndex((l) => TASKS_HEAD.test(l));
16
+ if (start < 0) return { lines, cut: false };
17
+ const out = lines.slice(0, start + 1);
18
+ let cut = false;
19
+ let i = start + 1;
20
+ while (i < lines.length) {
21
+ const l = lines[i]!;
22
+ if (/^To see (?:all tasks|more detail about a task)/.test(l)) {
23
+ i++;
24
+ continue;
25
+ }
26
+ // A group: its title, then one task per line up to a blank line.
27
+ const body: string[] = [];
28
+ let j = i + 1;
29
+ while (j < lines.length && lines[j] !== "") body.push(lines[j++]!);
30
+ if (l === "" || !body.length || !/tasks$/i.test(l)) {
31
+ out.push(l);
32
+ i++;
33
+ continue;
34
+ }
35
+ const line = `${l} (${body.length}): ${body.map((b) => b.split(" - ")[0]).join(" ")}`;
36
+ // The descriptions go: the full output keeps them.
37
+ cut = true;
38
+ out.push(cutLine(line, width));
39
+ i = j;
40
+ }
41
+ return { lines: out, cut };
42
+ }
43
+
44
+ const PROJECT = /^[|+\\ -]*[Pp]roject '(:[^']+)'/;
45
+
46
+ export function squeezeProjects(lines: string[], width: number): Rewritten {
47
+ const paths = new Set<string>();
48
+ let at = -1;
49
+ const rest: string[] = [];
50
+ const roots = new Set<string>();
51
+ for (const l of lines) {
52
+ const m = PROJECT.exec(l);
53
+ if (m) {
54
+ if (at < 0) at = rest.length;
55
+ paths.add(m[1]!);
56
+ } else if (/^Root project '/.test(l)) {
57
+ if (!roots.has(l)) rest.push(l);
58
+ roots.add(l);
59
+ } else rest.push(l);
60
+ }
61
+ if (paths.size < 5) return { lines, cut: false };
62
+ const groups = new Map<string, string[]>();
63
+ for (const p of [...paths].sort()) {
64
+ const [top, ...sub] = p.slice(1).split(":");
65
+ const names = groups.get(top!) ?? [];
66
+ if (sub.length) names.push(sub.join(":"));
67
+ groups.set(top!, names);
68
+ }
69
+ let cut = false;
70
+ const listed = [`${paths.size} projects`];
71
+ for (const [top, names] of groups) {
72
+ const line = names.length ? `:${top} (${names.length}): ${names.join(" ")}` : `:${top}`;
73
+ cut ||= line.length > width;
74
+ listed.push(cutLine(line, width));
75
+ }
76
+ rest.splice(at, 0, ...listed);
77
+ return { lines: rest, cut };
78
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Package installs (npm, pnpm, yarn, bun, pip, uv, CocoaPods, Bundler, pub): download and progress lines
3
+ * go, per-package lines collapse into one line each (`Collecting (9): requests, rich, …`),
4
+ * deprecation warnings become one list of names. Errors, the summary and anything not
5
+ * recognised stay where they were.
6
+ */
7
+ import { capLines, cutLine, DEFAULT_LIMITS, type Limits, type Squeezed } from "./elide.ts";
8
+ import { stripAnsi } from "./generic.ts";
9
+
10
+ type Rule = { re: RegExp; label: string; item: (m: RegExpExecArray) => string };
11
+
12
+ /** Lines that say nothing once the install is over. */
13
+ const DROP: RegExp[] = [
14
+ // npm
15
+ /^\d+ packages? (are|is) looking for funding$/,
16
+ /^ {2}run `npm fund` for details$/,
17
+ /^To address (all|issues that do not require attention|all issues possible)/,
18
+ /^ {2}npm audit fix/,
19
+ /^Some issues need review, and may require choosing$/,
20
+ /^a different dependency\.$/,
21
+ /^npm notice/,
22
+ /^(up to date|changed \d+ packages?), audited/,
23
+ // pip
24
+ /^ {2}Downloading \S+/,
25
+ /^ {2}Using cached \S+/,
26
+ /^ {2}Preparing metadata/,
27
+ /^ {2}(Installing|Getting requirements to) build/,
28
+ /^ {2}(Building|Created|Stored in directory)/,
29
+ /^Building wheels? for /,
30
+ /^Installing collected packages: /,
31
+ /^ {2}Attempting uninstall: /,
32
+ /^ {4}(Found existing installation|Uninstalling) /,
33
+ /^ {6}Successfully uninstalled /,
34
+ /^WARNING: You are using pip version/,
35
+ /^You should consider upgrading via/,
36
+ /^\[notice\] /,
37
+ /^\s*[━╸╺\-]+ [\d.]+\/[\d.]+ [kMG]?B /,
38
+ // bun, uv, pnpm, yarn
39
+ /^bun (add|install|update|remove) v[\d.]+/,
40
+ /^Resolving dependencies$/,
41
+ /^Resolved, downloaded and extracted/,
42
+ /^Saved lockfile$/,
43
+ /^(Resolved|Prepared|Audited|Uninstalled) \d+ packages? in /,
44
+ /^Progress: resolved \d+/,
45
+ /^Packages: [+-]\d+/,
46
+ /^[+-]+$/,
47
+ /^\[\d\/\d\] /,
48
+ /^(yarn (install|add) v|info (No lockfile|Lockfile|fsevents)|success Saved lockfile|Done in [\d.]+s)/,
49
+ // CocoaPods, pub
50
+ /^(Analyzing|Downloading) dependencies$/,
51
+ /^Fetching gem metadata from /,
52
+ /^(Generating Pods project|Integrating client project|Resolving dependencies\.\.\.|Downloading packages\.\.\.|Got dependencies!?)$/,
53
+ ];
54
+
55
+ /** Lines that become one counted line, at the place of the first one. */
56
+ const COLLECT: Rule[] = [
57
+ { re: /^npm warn deprecated (\S+): /, label: "npm warn deprecated", item: (m) => m[1]! },
58
+ { re: /^\s*WARN\s+deprecated (\S+): /, label: "pnpm WARN deprecated", item: (m) => m[1]! },
59
+ { re: /^warning (\S+(?: > \S+)*): .*deprecated/, label: "yarn warning deprecated", item: (m) => m[1]! },
60
+ { re: /^Collecting (\S+)/, label: "Collecting", item: (m) => m[1]! },
61
+ { re: /^Requirement already satisfied: (\S+)/, label: "Requirement already satisfied", item: (m) => m[1]! },
62
+ { re: /^installed (\S+)/, label: "installed", item: (m) => m[1]! },
63
+ { re: /^ ?\+ (\S+)(?: (\S+))?$/, label: "+", item: (m) => (m[2] ? `${m[1]} ${m[2]}` : m[1]!) },
64
+ { re: /^ - (\S+)(?: (\S+))?$/, label: "-", item: (m) => (m[2] ? `${m[1]} ${m[2]}` : m[1]!) },
65
+ { re: /^Installing (\S+) \(([^)]+)\)$/, label: "Installing", item: (m) => `${m[1]} ${m[2]}` },
66
+ { re: /^Using (\S+) \(([^)]+)\)$/, label: "Using", item: (m) => `${m[1]} ${m[2]}` },
67
+ // Bundler: `Using rake 13.0.6`, `Installing nokogiri 1.16.0 with native extensions`.
68
+ { re: /^Using (\S+) ([\w.]+)$/, label: "Using", item: (m) => `${m[1]} ${m[2]}` },
69
+ { re: /^Installing (\S+) ([\w.]+)(?: with native extensions)?$/, label: "Installing", item: (m) => `${m[1]} ${m[2]}` },
70
+ { re: /^Fetching (\S+) ([\w.]+)$/, label: "Fetching", item: (m) => `${m[1]} ${m[2]}` },
71
+ { re: /^ {2}(\S+) ([\w.+-]+) \(([\w.+-]+) available\)$/, label: "newer versions available", item: (m) => `${m[1]} ${m[2]}→${m[3]}` },
72
+ ];
73
+
74
+ /** Names shown per collected line before `… +N`. */
75
+ const SHOWN = 12;
76
+
77
+ export function compactInstall(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
78
+ const lines = stripAnsi(output)
79
+ .split("\n")
80
+ .map((l) => {
81
+ // A progress bar redrawn with `\r`: only its last frame counts.
82
+ const frame = l.replace(/\r+$/, "");
83
+ return frame.slice(frame.lastIndexOf("\r") + 1);
84
+ });
85
+ while (lines.length && lines.at(-1) === "") lines.pop();
86
+ const out: (string | { label: string; items: string[] })[] = [];
87
+ const groups = new Map<string, { label: string; items: string[] }>();
88
+ let touched = 0;
89
+ for (const l of lines) {
90
+ if (DROP.some((d) => d.test(l))) {
91
+ touched++;
92
+ continue;
93
+ }
94
+ const rule = COLLECT.find((r) => r.re.test(l));
95
+ if (rule) {
96
+ touched++;
97
+ const label = rule.label;
98
+ let g = groups.get(label);
99
+ if (!g) {
100
+ g = { label, items: [] };
101
+ groups.set(label, g);
102
+ out.push(g);
103
+ }
104
+ g.items.push(rule.item(rule.re.exec(l)!));
105
+ continue;
106
+ }
107
+ if (l === "" && (out.length === 0 || out.at(-1) === "")) continue;
108
+ out.push(l);
109
+ }
110
+ if (!touched) return undefined;
111
+ // A collected line keeps the names, not the rest of each line (paths, deprecation notes).
112
+ let cut = false;
113
+ const text = out.map((o) => {
114
+ if (typeof o === "string") return cutLine(o, limits.lineChars);
115
+ const shown = o.items.slice(0, SHOWN).join(", ");
116
+ if (o.items.length > SHOWN) cut = true;
117
+ return `${o.label} (${o.items.length}): ${shown}${o.items.length > SHOWN ? `, … +${o.items.length - SHOWN}` : ""}`;
118
+ });
119
+ while (text.at(-1) === "") text.pop();
120
+ const capped = capLines(text, limits.total);
121
+ return { text: capped.text, truncated: cut || capped.truncated || groups.size > 0 };
122
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `javac` output: each error keeps its source line, caret and `symbol:` / `location:` lines
3
+ * (what fixing it needs); warnings are grouped by kind and message, each location once
4
+ * (`warning [rawtypes]: found raw type: List (3)`), their source lines and "where E is a
5
+ * type-variable" explanations go, as does the advice to recompile with -Xlint. Anything
6
+ * that is not javac output passes the whole of it through.
7
+ */
8
+ import { capLines, cutLine, DEFAULT_LIMITS, type Limits, type Squeezed } from "./elide.ts";
9
+ import { shortenPaths } from "./gradle.ts";
10
+ import { type Diag, renderDiags } from "./lint.ts";
11
+
12
+ const HEAD = /^(\S.*?\.java):(\d+): (error|warning): (?:\[([\w-]+)\] )?(.*)$/;
13
+ const TOTAL = /^\d+ (?:errors?|warnings?)$/;
14
+ const NOTE = /^Note: /;
15
+
16
+ export function compactJavac(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
17
+ const lines = output.split("\n");
18
+ while (lines.length && lines.at(-1) === "") lines.pop();
19
+ const errors: string[] = [];
20
+ const warnings: Diag[] = [];
21
+ const totals: string[] = [];
22
+ let current: "error" | "warning" | undefined;
23
+ for (const l of lines) {
24
+ const m = HEAD.exec(l);
25
+ if (m) {
26
+ current = m[3] as "error" | "warning";
27
+ if (current === "error") errors.push(l);
28
+ else warnings.push({ file: m[1]!, line: m[2]!, severity: "warning", rule: m[4], message: m[5]! });
29
+ } else if (TOTAL.test(l)) totals.push(l);
30
+ else if (NOTE.test(l)) {
31
+ // "uses or overrides a deprecated API" says something; "Recompile with -Xlint" does not.
32
+ if (!/Recompile with -Xlint/.test(l)) totals.push(l);
33
+ } else if (!current) return undefined;
34
+ // Under an error: the source line, the caret and symbol/location stay; type-variable notes go.
35
+ else if (current === "error" && !/^\s+(?:where \w+ is a type-variable|\w+ extends \w+ declared in)/.test(l)) errors.push(l);
36
+ }
37
+ if (!errors.length && !warnings.length) return undefined;
38
+ shortenPaths(errors);
39
+ const out = errors.map((l) => cutLine(l, limits.lineChars));
40
+ if (warnings.length) out.push(...renderDiags({ tool: "javac", diags: warnings, notes: [] }, { ...limits, total: Number.MAX_SAFE_INTEGER }).text.split("\n"));
41
+ out.push(...totals);
42
+ const capped = capLines(out, limits.total);
43
+ return { text: capped.text, truncated: warnings.length > 0 || capped.truncated };
44
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * jest and vitest: passing tests (`✓`) and passing files go, skipped tests are counted;
3
+ * failures with their diff and source frame, and the totals, stay.
4
+ */
5
+ import { capLines, cutLine, DEFAULT_LIMITS, type Limits, type Squeezed } from "./elide.ts";
6
+ import { stripAnsi } from "./generic.ts";
7
+
8
+ const TOTALS = /^\s*(Tests|Test Files|Test Suites):?\s+\d+ (passed|failed|skipped|todo|total)/;
9
+ const PASS = [
10
+ /^\s*[✓√] /,
11
+ /^PASS /,
12
+ /^ RUN {2}v[\d.]+/,
13
+ /^\s+Start at {2}/,
14
+ /^Snapshots: {3}0 total$/,
15
+ /^Ran all test suites\.$/,
16
+ ];
17
+ const SKIP = /^\s*[↓○] (skipped )?/;
18
+
19
+ export function compactJest(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
20
+ const lines = stripAnsi(output).split("\n");
21
+ while (lines.length && lines.at(-1) === "") lines.pop();
22
+ if (!lines.some((l) => TOTALS.test(l))) return undefined;
23
+ const kept: string[] = [];
24
+ let skipped = 0;
25
+ for (const l of lines) {
26
+ if (PASS.some((p) => p.test(l))) continue;
27
+ if (SKIP.test(l)) {
28
+ skipped++;
29
+ continue;
30
+ }
31
+ if (l.trim() === "" && (kept.length === 0 || kept.at(-1) === "")) continue;
32
+ // vitest's full-width `⎯⎯⎯ Failed Tests 2 ⎯⎯⎯` rules keep two of their dashes.
33
+ kept.push(cutLine(l.trim() === "" ? "" : l.replace(/⎯{3,}/g, "⎯⎯"), limits.lineChars));
34
+ }
35
+ while (kept.at(-1) === "") kept.pop();
36
+ if (skipped) kept.push(`(${skipped} skipped not listed)`);
37
+ return capLines(kept, limits.total);
38
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What the JVM and its usual libraries print on every run of Gradle, Maven or a test JVM,
3
+ * and that says nothing about the build: class-data sharing, dynamic agents (Mockito,
4
+ * ByteBuddy), SLF4J without a binding.
5
+ */
6
+ export const JVM_NOISE = [
7
+ /^OpenJDK 64-Bit Server VM warning: Sharing is only supported/,
8
+ /^WARNING: A Java agent has been loaded dynamically/,
9
+ /^WARNING: If a serviceability tool is (?:not )?in use/,
10
+ /^WARNING: Dynamic loading of agents will be disallowed/,
11
+ /^WARNING: A terminally deprecated method in sun\.misc\.Unsafe/,
12
+ /^WARNING: sun\.misc\.Unsafe::\w+ (?:has been called|will be removed)/,
13
+ /^WARNING: Please consider reporting this to the maintainers/,
14
+ /^Mockito is currently self-attaching to enable the inline-mock-maker/,
15
+ /^SLF4J(?:\(\w\))?: (?:No SLF4J providers|Defaulting to no-operation|See https?:\/\/www\.slf4j\.org|Failed to load class|Class path contains multiple)/,
16
+ ];
17
+
18
+ export const isJvmNoise = (line: string): boolean => JVM_NOISE.some((n) => n.test(line));