@chappibunny/repolens 1.16.2 → 1.16.4
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/CHANGELOG.md +57 -0
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/ai/generate-sections.js +23 -4
- package/src/analyzers/complexity-analyzer.js +17 -4
- package/src/analyzers/context-builder.js +2 -1
- package/src/analyzers/cycle-priority.js +3 -0
- package/src/analyzers/dependency-graph.js +265 -34
- package/src/analyzers/flow-inference.js +10 -1
- package/src/analyzers/graphql-analyzer.js +80 -32
- package/src/analyzers/import-parsers.js +47 -14
- package/src/analyzers/language-coverage.js +222 -0
- package/src/analyzers/ts-elision.js +921 -0
- package/src/analyzers/workspace-packages.js +105 -0
- package/src/badge.js +59 -18
- package/src/core/scan.js +10 -4
- package/src/docs/generate-doc-set.js +11 -3
- package/src/help.js +8 -3
- package/src/renderers/render.js +14 -2
- package/src/renderers/renderAnalysis.js +34 -2
- package/src/utils/js-lexer.js +146 -0
|
@@ -164,6 +164,23 @@ function localNames(inner) {
|
|
|
164
164
|
return names;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** `{ a, b as c, type D }` → [{ imported: "a", local: "a" }, { imported: "b", local: "c" }]. */
|
|
168
|
+
function braceBindings(inner) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const part of splitTopCommas(inner)) {
|
|
171
|
+
const trimmed = part.trim().replace(/\s+/g, " ");
|
|
172
|
+
if (!trimmed || /^type\s+/.test(trimmed)) continue;
|
|
173
|
+
const asMatch = trimmed.match(/^([A-Za-z_$][\w$]*|"[^"]*"|'[^']*')\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
174
|
+
if (asMatch) {
|
|
175
|
+
out.push({ imported: asMatch[1].replace(/^["']|["']$/g, ""), local: asMatch[2] });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const id = trimmed.match(/^([A-Za-z_$][\w$]*)$/);
|
|
179
|
+
if (id) out.push({ imported: id[1], local: id[1] });
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
function readAlias(source, i) {
|
|
168
185
|
let j = skipWs(source, i);
|
|
169
186
|
const word = readWord(source, j);
|
|
@@ -206,7 +223,7 @@ function readFromClause(source, i) {
|
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
/**
|
|
209
|
-
* @returns {{ specifier: string, typeOnly: boolean, names: string[] }[]}
|
|
226
|
+
* @returns {{ specifier: string, typeOnly: boolean, names: string[], kind: string, bindings: { imported: string, local: string|null }[] }[]}
|
|
210
227
|
* `names` are the local bindings (`import { a as b }` → `b`) so callers can
|
|
211
228
|
* count `b(` as a use of this import. Type-only imports still list names.
|
|
212
229
|
*/
|
|
@@ -215,7 +232,12 @@ export function extractJsImports(source) {
|
|
|
215
232
|
const n = source.length;
|
|
216
233
|
let i = 0;
|
|
217
234
|
|
|
218
|
-
|
|
235
|
+
// `kind`: "import" (has local bindings), "reexport" (`export { a } from`),
|
|
236
|
+
// "reexport-all" (`export * from`), "side-effect" (`import "x"`), or "dynamic"
|
|
237
|
+
// (`import()` / `require()`). `bindings` pair each imported name with its local
|
|
238
|
+
// name (`*` for a namespace import, `default` for a default import); for a
|
|
239
|
+
// re-export, `local` is null.
|
|
240
|
+
function add(specifier, typeOnly, names = [], kind = "import", bindings = []) {
|
|
219
241
|
if (typeof specifier !== "string") return;
|
|
220
242
|
const locals = [];
|
|
221
243
|
const seen = new Set();
|
|
@@ -224,7 +246,7 @@ export function extractJsImports(source) {
|
|
|
224
246
|
seen.add(name);
|
|
225
247
|
locals.push(name);
|
|
226
248
|
}
|
|
227
|
-
imports.push({ specifier, typeOnly: Boolean(typeOnly), names: locals });
|
|
249
|
+
imports.push({ specifier, typeOnly: Boolean(typeOnly), names: locals, kind, bindings: typeOnly ? [] : bindings });
|
|
228
250
|
}
|
|
229
251
|
|
|
230
252
|
function parseRequire(at) {
|
|
@@ -234,7 +256,7 @@ export function extractJsImports(source) {
|
|
|
234
256
|
if (!lit) return at;
|
|
235
257
|
const after = skipWs(source, lit.next);
|
|
236
258
|
if (source[after] !== ")") return at;
|
|
237
|
-
add(lit.value, false);
|
|
259
|
+
add(lit.value, false, [], "dynamic");
|
|
238
260
|
return after + 1;
|
|
239
261
|
}
|
|
240
262
|
|
|
@@ -245,7 +267,7 @@ export function extractJsImports(source) {
|
|
|
245
267
|
if (!lit) return at;
|
|
246
268
|
const after = skipWs(source, lit.next);
|
|
247
269
|
if (source[after] !== ")") return at;
|
|
248
|
-
add(lit.value, false);
|
|
270
|
+
add(lit.value, false, [], "dynamic");
|
|
249
271
|
return after + 1;
|
|
250
272
|
}
|
|
251
273
|
if (source[j] === ".") return at;
|
|
@@ -264,22 +286,26 @@ export function extractJsImports(source) {
|
|
|
264
286
|
const clause = readFromClause(source, close + 1);
|
|
265
287
|
if (!clause) return close + 1;
|
|
266
288
|
const onlyTypes = typeOnly || braceIsTypeOnly(inner);
|
|
267
|
-
add(clause.value, onlyTypes, typeOnly ? [] : localNames(inner));
|
|
289
|
+
add(clause.value, onlyTypes, typeOnly ? [] : localNames(inner), "import", typeOnly ? [] : braceBindings(inner));
|
|
268
290
|
return clause.next;
|
|
269
291
|
}
|
|
270
292
|
|
|
271
293
|
if (source[j] === "*" || isIdentStart(source[j]) || source[j] === "'" || source[j] === '"') {
|
|
272
294
|
if (source[j] === "'" || source[j] === '"') {
|
|
273
295
|
const lit = readJsString(source, j);
|
|
274
|
-
if (lit) add(lit.value, typeOnly);
|
|
296
|
+
if (lit) add(lit.value, typeOnly, [], "side-effect");
|
|
275
297
|
return lit ? lit.next : j;
|
|
276
298
|
}
|
|
277
299
|
// default binding, namespace, or `name, { ... }`
|
|
278
300
|
let sawValue = !typeOnly;
|
|
279
301
|
const names = [];
|
|
302
|
+
const bindings = [];
|
|
280
303
|
if (source[j] !== "*") {
|
|
281
304
|
const binding = readWord(source, j);
|
|
282
|
-
if (binding.word && !typeOnly)
|
|
305
|
+
if (binding.word && !typeOnly) {
|
|
306
|
+
names.push(binding.word);
|
|
307
|
+
bindings.push({ imported: "default", local: binding.word });
|
|
308
|
+
}
|
|
283
309
|
j = skipWs(source, binding.next);
|
|
284
310
|
if (source[j] === ",") {
|
|
285
311
|
sawValue = !typeOnly;
|
|
@@ -288,7 +314,10 @@ export function extractJsImports(source) {
|
|
|
288
314
|
} else {
|
|
289
315
|
j = skipWs(source, j + 1);
|
|
290
316
|
const alias = readAlias(source, j);
|
|
291
|
-
if (alias.name && !typeOnly)
|
|
317
|
+
if (alias.name && !typeOnly) {
|
|
318
|
+
names.push(alias.name);
|
|
319
|
+
bindings.push({ imported: "*", local: alias.name });
|
|
320
|
+
}
|
|
292
321
|
j = skipWs(source, alias.next);
|
|
293
322
|
}
|
|
294
323
|
if (source[j] === "{") {
|
|
@@ -298,13 +327,16 @@ export function extractJsImports(source) {
|
|
|
298
327
|
const clause = readFromClause(source, close + 1);
|
|
299
328
|
if (!clause) return close + 1;
|
|
300
329
|
const onlyTypes = typeOnly || (!sawValue && braceIsTypeOnly(inner));
|
|
301
|
-
if (!onlyTypes)
|
|
302
|
-
|
|
330
|
+
if (!onlyTypes) {
|
|
331
|
+
names.push(...localNames(inner));
|
|
332
|
+
bindings.push(...braceBindings(inner));
|
|
333
|
+
}
|
|
334
|
+
add(clause.value, onlyTypes, names, "import", bindings);
|
|
303
335
|
return clause.next;
|
|
304
336
|
}
|
|
305
337
|
const clause = readFromClause(source, j);
|
|
306
338
|
if (!clause) return at;
|
|
307
|
-
add(clause.value, typeOnly, typeOnly ? [] : names);
|
|
339
|
+
add(clause.value, typeOnly, typeOnly ? [] : names, "import", bindings);
|
|
308
340
|
return clause.next;
|
|
309
341
|
}
|
|
310
342
|
|
|
@@ -322,7 +354,7 @@ export function extractJsImports(source) {
|
|
|
322
354
|
if (source[j] === "*") {
|
|
323
355
|
const clause = readFromClause(source, j + 1);
|
|
324
356
|
if (!clause) return at;
|
|
325
|
-
add(clause.value, typeOnly);
|
|
357
|
+
add(clause.value, typeOnly, [], "reexport-all");
|
|
326
358
|
return clause.next;
|
|
327
359
|
}
|
|
328
360
|
if (source[j] === "{") {
|
|
@@ -331,7 +363,8 @@ export function extractJsImports(source) {
|
|
|
331
363
|
const inner = source.slice(j + 1, close);
|
|
332
364
|
const clause = readFromClause(source, close + 1);
|
|
333
365
|
if (!clause) return close + 1;
|
|
334
|
-
|
|
366
|
+
const bindings = braceBindings(inner).map((item) => ({ imported: item.imported, local: null }));
|
|
367
|
+
add(clause.value, typeOnly || braceIsTypeOnly(inner), [], "reexport", bindings);
|
|
335
368
|
return clause.next;
|
|
336
369
|
}
|
|
337
370
|
return at;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Which scanned source files the import graph and code health can read.
|
|
2
|
+
// RepoLens detects and scans many languages, but it builds import graphs and scores code
|
|
3
|
+
// health only for JavaScript/TypeScript, Python, and Go. A C# or Java repository must not
|
|
4
|
+
// get "no circular dependencies" or "health 100/100" from one incidental docs script.
|
|
5
|
+
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { LANGUAGE_PROFILES } from "../core/languages.js";
|
|
8
|
+
import { isNonProductPath } from "../utils/path-roles.js";
|
|
9
|
+
|
|
10
|
+
export const ANALYZED_LANGUAGES_TEXT = "JavaScript/TypeScript, Python, and Go";
|
|
11
|
+
|
|
12
|
+
const ANALYZED_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".pyw", ".go"]);
|
|
13
|
+
|
|
14
|
+
// A code-health badge is one number for the whole repository. Below this share of
|
|
15
|
+
// product source files in analyzed languages, `repolens badge` refuses to print it.
|
|
16
|
+
// The docs still show the partial graph and scores with a coverage note.
|
|
17
|
+
export const BADGE_MIN_SHARE = 0.1;
|
|
18
|
+
|
|
19
|
+
// Vendored or third-party trees are not the repository's own code, in any language.
|
|
20
|
+
export const VENDORED_DIRECTORIES = new Set([
|
|
21
|
+
"vendor", "vendored", "third_party", "third-party", "thirdparty", "3rdparty",
|
|
22
|
+
"deps", "external", "extern", "node_modules", "bower_components",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const LABEL_BY_EXTENSION = new Map();
|
|
26
|
+
LABEL_BY_EXTENSION.set(".vue", "Vue");
|
|
27
|
+
LABEL_BY_EXTENSION.set(".svelte", "Svelte");
|
|
28
|
+
const COMPANION_LABELS_LIST = ["Vue", "Svelte"];
|
|
29
|
+
for (const profile of LANGUAGE_PROFILES) {
|
|
30
|
+
const label = profile.id === "java" ? "Java/Kotlin" : profile.id === "cpp" ? "C/C++" : profile.label;
|
|
31
|
+
for (const ext of profile.extensions) {
|
|
32
|
+
if (!LABEL_BY_EXTENSION.has(ext)) LABEL_BY_EXTENSION.set(ext, label);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isDeclarationFile(file) {
|
|
37
|
+
return /\.d\.[cm]?ts$/.test(file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isVendoredPath(file) {
|
|
41
|
+
const parts = String(file || "").replace(/\\/g, "/").toLowerCase().split("/");
|
|
42
|
+
parts.pop();
|
|
43
|
+
return parts.some((segment) => VENDORED_DIRECTORIES.has(segment));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Product source = not a test, docs, tooling, example, or vendored path.
|
|
48
|
+
*
|
|
49
|
+
* - "none": no source files in any known language
|
|
50
|
+
* - "full": every product source file is in an analyzed language
|
|
51
|
+
* - "partial": analyzed-language product code exists next to other languages; analysis
|
|
52
|
+
* runs and every page says what it did not cover
|
|
53
|
+
* - "unsupported": no analyzed-language product code at all (only a docs script, a test
|
|
54
|
+
* helper, or vendored files); graph and health are not built
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} files - scanned file paths
|
|
57
|
+
*/
|
|
58
|
+
export function computeLanguageCoverage(files) {
|
|
59
|
+
let analyzedFiles = 0;
|
|
60
|
+
let analyzedProductFiles = 0;
|
|
61
|
+
let unanalyzedFiles = 0;
|
|
62
|
+
let unanalyzedProductFiles = 0;
|
|
63
|
+
let vendoredFiles = 0;
|
|
64
|
+
let vendoredAnalyzedFiles = 0;
|
|
65
|
+
const byLabelProduct = new Map();
|
|
66
|
+
const byLabelAll = new Map();
|
|
67
|
+
|
|
68
|
+
for (const raw of files || []) {
|
|
69
|
+
const file = String(raw).replace(/\\/g, "/");
|
|
70
|
+
if (isDeclarationFile(file)) continue;
|
|
71
|
+
const ext = path.posix.extname(file).toLowerCase();
|
|
72
|
+
const analyzed = ANALYZED_EXTENSIONS.has(ext);
|
|
73
|
+
const label = analyzed ? null : LABEL_BY_EXTENSION.get(ext);
|
|
74
|
+
if (!analyzed && !label) continue;
|
|
75
|
+
if (isVendoredPath(file)) {
|
|
76
|
+
vendoredFiles += 1;
|
|
77
|
+
if (analyzed) vendoredAnalyzedFiles += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const product = !isNonProductPath(file);
|
|
81
|
+
if (analyzed) {
|
|
82
|
+
analyzedFiles += 1;
|
|
83
|
+
if (product) analyzedProductFiles += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
unanalyzedFiles += 1;
|
|
87
|
+
byLabelAll.set(label, (byLabelAll.get(label) || 0) + 1);
|
|
88
|
+
if (product) {
|
|
89
|
+
unanalyzedProductFiles += 1;
|
|
90
|
+
byLabelProduct.set(label, (byLabelProduct.get(label) || 0) + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Product files decide everything; a tree with no product files at all (only tests,
|
|
95
|
+
// docs, or tooling) falls back to every non-vendored file.
|
|
96
|
+
const useProduct = analyzedProductFiles + unanalyzedProductFiles > 0;
|
|
97
|
+
const analyzedBase = useProduct ? analyzedProductFiles : analyzedFiles;
|
|
98
|
+
const unanalyzedBase = useProduct ? unanalyzedProductFiles : unanalyzedFiles;
|
|
99
|
+
const byLabel = useProduct ? byLabelProduct : byLabelAll;
|
|
100
|
+
const totalBase = analyzedBase + unanalyzedBase;
|
|
101
|
+
const share = totalBase > 0 ? analyzedBase / totalBase : null;
|
|
102
|
+
// Vue and Svelte components sit next to analyzed JavaScript/TypeScript. They are not
|
|
103
|
+
// analyzed, but they do not count against the badge share: a component library with
|
|
104
|
+
// two .ts files and forty .vue files keeps its (partial) score.
|
|
105
|
+
const companionBase = COMPANION_LABELS_LIST.reduce((sum, label) => sum + (byLabel.get(label) || 0), 0);
|
|
106
|
+
const badgeShare = totalBase - companionBase > 0 ? analyzedBase / (totalBase - companionBase) : null;
|
|
107
|
+
|
|
108
|
+
const unanalyzedLanguages = [...byLabel.entries()]
|
|
109
|
+
.map(([label, count]) => ({ label, files: count }))
|
|
110
|
+
.sort((a, b) => b.files - a.files || a.label.localeCompare(b.label));
|
|
111
|
+
|
|
112
|
+
let status;
|
|
113
|
+
if (totalBase === 0) status = "none";
|
|
114
|
+
else if (unanalyzedBase === 0) status = "full";
|
|
115
|
+
else if (analyzedBase === 0) status = "unsupported";
|
|
116
|
+
else status = "partial";
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
status,
|
|
120
|
+
analyzedFiles,
|
|
121
|
+
analyzedProductFiles,
|
|
122
|
+
unanalyzedFiles,
|
|
123
|
+
unanalyzedProductFiles,
|
|
124
|
+
vendoredFiles,
|
|
125
|
+
vendoredAnalyzedFiles,
|
|
126
|
+
productBased: useProduct,
|
|
127
|
+
analyzedBase,
|
|
128
|
+
totalBase,
|
|
129
|
+
unanalyzedLanguages,
|
|
130
|
+
share,
|
|
131
|
+
companionBase,
|
|
132
|
+
badgeShare,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whether one code-health number may stand for the repository. */
|
|
137
|
+
export function badgeCoverageOk(coverage) {
|
|
138
|
+
if (!coverage) return true;
|
|
139
|
+
if (coverage.status === "full" || coverage.status === "none") return true;
|
|
140
|
+
if (coverage.status === "unsupported") return false;
|
|
141
|
+
const share = coverage.badgeShare ?? coverage.share ?? 0;
|
|
142
|
+
return share >= BADGE_MIN_SHARE;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Short qualifier for claims made from the graph ("no circular dependencies"), or "".
|
|
147
|
+
* Partial: " (analyzed files only: 50 of 650 product source files; C/C++ not analyzed)".
|
|
148
|
+
*/
|
|
149
|
+
export function coverageScope(coverage) {
|
|
150
|
+
if (!coverage || coverage.status !== "partial") return "";
|
|
151
|
+
return ` (analyzed files only: ${coverage.analyzedBase} of ${coverage.totalBase} ${coverage.productBased ? "product " : ""}source files; ${unanalyzedLanguageNames(coverage)} not analyzed)`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** "C# (780 files)" / "Java/Kotlin (247 files) and C (3 files)". */
|
|
155
|
+
export function describeUnanalyzedLanguages(coverage, limit = 3) {
|
|
156
|
+
const list = (coverage?.unanalyzedLanguages || []).slice(0, limit)
|
|
157
|
+
.map(({ label, files }) => `${label} (${files} ${coverage?.productBased ? "product " : ""}file${files === 1 ? "" : "s"})`);
|
|
158
|
+
if (list.length === 0) return "";
|
|
159
|
+
if (list.length === 1) return list[0];
|
|
160
|
+
return `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Short language names only: "C#", "Java/Kotlin and Scala". */
|
|
164
|
+
export function unanalyzedLanguageNames(coverage, limit = 3) {
|
|
165
|
+
const list = (coverage?.unanalyzedLanguages || []).slice(0, limit).map(({ label }) => label);
|
|
166
|
+
if (list.length <= 1) return list[0] || "";
|
|
167
|
+
return `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Files an analyzer should read. When the analyzed languages are only incidental
|
|
172
|
+
* (status "unsupported"), none: the graph or score would describe a docs script,
|
|
173
|
+
* not the repository.
|
|
174
|
+
*/
|
|
175
|
+
export function filesForAnalysis(files, coverage = computeLanguageCoverage(files)) {
|
|
176
|
+
if (coverage.status === "unsupported") return [];
|
|
177
|
+
return files;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function verbFor(coverage) {
|
|
181
|
+
return (coverage?.unanalyzedLanguages || []).length > 1 ? "are" : "is";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Markdown lines that say what a page did not analyze.
|
|
186
|
+
* `what` is "import analysis" or "code health".
|
|
187
|
+
* Returns [] for full coverage or when there is nothing to say.
|
|
188
|
+
*/
|
|
189
|
+
export function coverageNotice(coverage, what) {
|
|
190
|
+
if (!coverage) return [];
|
|
191
|
+
const langs = describeUnanalyzedLanguages(coverage);
|
|
192
|
+
if (coverage.status === "unsupported") {
|
|
193
|
+
const n = (coverage.analyzedFiles || 0) + (coverage.vendoredAnalyzedFiles || 0);
|
|
194
|
+
const incidental = n > 0
|
|
195
|
+
? ` ${n === 1 ? "One file" : `${n} files`} in a supported language ${n === 1 ? "was" : "were"} found only in docs, tests, tooling, examples, or vendored directories, so ${n === 1 ? "it was" : "they were"} not ${what === "code health" ? "scored" : "graphed"} as if ${n === 1 ? "it were" : "they were"} the codebase.`
|
|
196
|
+
: "";
|
|
197
|
+
return [
|
|
198
|
+
`> ${langs} ${verbFor(coverage)} not supported for ${what} yet. RepoLens ${what === "code health" ? "scores" : "builds import graphs for"} ${ANALYZED_LANGUAGES_TEXT} only.`,
|
|
199
|
+
"",
|
|
200
|
+
`Modules and file structure for this repository are still documented in the other pages. This is a language limitation, not a problem with your \`scan.include\` patterns.${incidental}`,
|
|
201
|
+
"",
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
if (coverage.status === "partial") {
|
|
205
|
+
const pct = coverage.share != null ? Math.round(coverage.share * 100) : null;
|
|
206
|
+
return [
|
|
207
|
+
`> **Partial coverage:** ${what} covers ${coverage.analyzedBase} of ${coverage.totalBase} ${coverage.productBased ? "product " : ""}source files${pct != null ? ` (${pct}%)` : ""}, the ${ANALYZED_LANGUAGES_TEXT} ones. ${langs} ${verbFor(coverage)} not analyzed, so results below say nothing about ${coverage.unanalyzedLanguages.length > 1 ? "those languages" : "that language"}.`,
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One plain-text line for logs and the badge CLI. */
|
|
215
|
+
export function coverageLine(coverage, what) {
|
|
216
|
+
if (!coverage || (coverage.status !== "unsupported" && coverage.status !== "partial")) return "";
|
|
217
|
+
const langs = describeUnanalyzedLanguages(coverage);
|
|
218
|
+
if (coverage.status === "unsupported") {
|
|
219
|
+
return `${langs} ${verbFor(coverage)} not supported for ${what} yet (${ANALYZED_LANGUAGES_TEXT} only).`;
|
|
220
|
+
}
|
|
221
|
+
return `${what} covers ${ANALYZED_LANGUAGES_TEXT} only; ${langs} ${verbFor(coverage)} not analyzed.`;
|
|
222
|
+
}
|