@danielsimonjr/repo-tools 2.1.0 → 2.2.0
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 +97 -0
- package/README.md +56 -2
- package/dist/cli.js +1798 -351
- package/dist/tree-sitter-tsx-bmyq0z5a.wasm +0 -0
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@danielsimonjr/repo-tools",
|
|
5
|
-
version: "2.
|
|
5
|
+
version: "2.2.0",
|
|
6
6
|
description: "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
|
|
7
7
|
type: "module",
|
|
8
8
|
license: "MIT",
|
|
@@ -35,7 +35,9 @@ var package_default = {
|
|
|
35
35
|
privacy: "bun scripts/privacy-check.ts",
|
|
36
36
|
build: "bun scripts/build.ts",
|
|
37
37
|
compile: "bun scripts/build.ts --compile",
|
|
38
|
-
smoke: "bun scripts/smoke.ts"
|
|
38
|
+
smoke: "bun scripts/smoke.ts",
|
|
39
|
+
"audit:ledger": "bun scripts/fix-ledger-audit.ts",
|
|
40
|
+
"check:design": "bun scripts/design-refs.ts"
|
|
39
41
|
},
|
|
40
42
|
devDependencies: {
|
|
41
43
|
"@biomejs/biome": "2.5.14",
|
|
@@ -52,7 +54,7 @@ var package_default = {
|
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
// src/check/index.ts
|
|
55
|
-
import { readdirSync as
|
|
57
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync17 } from "node:fs";
|
|
56
58
|
import { join as join20, resolve as resolve5 } from "node:path";
|
|
57
59
|
|
|
58
60
|
// src/config.ts
|
|
@@ -1747,13 +1749,7 @@ function outputPath(outputDir, relPath) {
|
|
|
1747
1749
|
}
|
|
1748
1750
|
|
|
1749
1751
|
// src/map/artifacts.ts
|
|
1750
|
-
import {
|
|
1751
|
-
mkdirSync as mkdirSync2,
|
|
1752
|
-
readdirSync as readdirSync4,
|
|
1753
|
-
readFileSync as readFileSync9,
|
|
1754
|
-
statSync as statSync5,
|
|
1755
|
-
writeFileSync as writeFileSync2
|
|
1756
|
-
} from "node:fs";
|
|
1752
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1757
1753
|
import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
|
|
1758
1754
|
|
|
1759
1755
|
// src/map/cycles.ts
|
|
@@ -5996,6 +5992,7 @@ function detectWorkspaces(root, warn = () => {}) {
|
|
|
5996
5992
|
// src/py.ts
|
|
5997
5993
|
var SPACE_BODY = "\\t\\n\\v\\f\\r\\x1c-\\x1f \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
5998
5994
|
var S = `[${SPACE_BODY}]`;
|
|
5995
|
+
var NON_SPACE = `[^${SPACE_BODY}]`;
|
|
5999
5996
|
var W = "[\\p{L}\\p{N}_]";
|
|
6000
5997
|
var B = `(?:(?<=${W})(?!${W})|(?<!${W})(?=${W}))`;
|
|
6001
5998
|
var D = "\\p{Nd}";
|
|
@@ -6280,7 +6277,7 @@ function toParsedFiles(graph, root, options = {}) {
|
|
|
6280
6277
|
|
|
6281
6278
|
// src/map/discovery.ts
|
|
6282
6279
|
import { spawnSync } from "node:child_process";
|
|
6283
|
-
import { lstatSync as lstatSync3,
|
|
6280
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
|
|
6284
6281
|
import { join as join10 } from "node:path";
|
|
6285
6282
|
var SOURCE_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6286
6283
|
var PYTHON_SUFFIXES = new Set([".py"]);
|
|
@@ -6381,7 +6378,7 @@ function isReparsePoint(path) {
|
|
|
6381
6378
|
}
|
|
6382
6379
|
function entries(dir) {
|
|
6383
6380
|
try {
|
|
6384
|
-
return
|
|
6381
|
+
return readdirHook.entries(dir);
|
|
6385
6382
|
} catch {
|
|
6386
6383
|
return [];
|
|
6387
6384
|
}
|
|
@@ -6600,7 +6597,7 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
|
|
|
6600
6597
|
}
|
|
6601
6598
|
|
|
6602
6599
|
// src/map/graph.ts
|
|
6603
|
-
import {
|
|
6600
|
+
import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
|
|
6604
6601
|
import { basename as basename3, join as join11, resolve as resolve3 } from "node:path";
|
|
6605
6602
|
|
|
6606
6603
|
// node_modules/smol-toml/dist/error.js
|
|
@@ -7579,6 +7576,9 @@ import { fileURLToPath } from "node:url";
|
|
|
7579
7576
|
// node_modules/tree-sitter-python/tree-sitter-python.wasm
|
|
7580
7577
|
var tree_sitter_python_default = "./tree-sitter-python-d56ewc8c.wasm";
|
|
7581
7578
|
|
|
7579
|
+
// node_modules/tree-sitter-typescript/tree-sitter-tsx.wasm
|
|
7580
|
+
var tree_sitter_tsx_default = "./tree-sitter-tsx-bmyq0z5a.wasm";
|
|
7581
|
+
|
|
7582
7582
|
// node_modules/tree-sitter-typescript/tree-sitter-typescript.wasm
|
|
7583
7583
|
var tree_sitter_typescript_default = "./tree-sitter-typescript-zxjzwt75.wasm";
|
|
7584
7584
|
|
|
@@ -10867,6 +10867,7 @@ var web_tree_sitter_default2 = "./web-tree-sitter-grrd7nak.wasm";
|
|
|
10867
10867
|
// src/map/grammars.ts
|
|
10868
10868
|
var WASM = {
|
|
10869
10869
|
typescript: tree_sitter_typescript_default,
|
|
10870
|
+
tsx: tree_sitter_tsx_default,
|
|
10870
10871
|
python: tree_sitter_python_default
|
|
10871
10872
|
};
|
|
10872
10873
|
function assetBytes(path) {
|
|
@@ -12431,7 +12432,7 @@ function rglobParts(root, suffix) {
|
|
|
12431
12432
|
const go = (dir, parts2) => {
|
|
12432
12433
|
let entries;
|
|
12433
12434
|
try {
|
|
12434
|
-
entries =
|
|
12435
|
+
entries = readdirHook.entries(dir);
|
|
12435
12436
|
} catch {
|
|
12436
12437
|
return;
|
|
12437
12438
|
}
|
|
@@ -12931,7 +12932,7 @@ function csprojParts(root) {
|
|
|
12931
12932
|
const go = (dir, parts2) => {
|
|
12932
12933
|
let entries;
|
|
12933
12934
|
try {
|
|
12934
|
-
entries =
|
|
12935
|
+
entries = readdirHook.entries(dir);
|
|
12935
12936
|
} catch {
|
|
12936
12937
|
return;
|
|
12937
12938
|
}
|
|
@@ -12966,7 +12967,7 @@ function csprojMap(root) {
|
|
|
12966
12967
|
}
|
|
12967
12968
|
function subdirs(dir) {
|
|
12968
12969
|
try {
|
|
12969
|
-
return
|
|
12970
|
+
return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join12(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
|
|
12970
12971
|
} catch {
|
|
12971
12972
|
return [];
|
|
12972
12973
|
}
|
|
@@ -15074,7 +15075,7 @@ function readDocs(dir, label) {
|
|
|
15074
15075
|
}
|
|
15075
15076
|
let entries;
|
|
15076
15077
|
try {
|
|
15077
|
-
entries =
|
|
15078
|
+
entries = readdirSync2(dir);
|
|
15078
15079
|
} catch {
|
|
15079
15080
|
throw new Error(`cannot read the docs folder ${label}`);
|
|
15080
15081
|
}
|
|
@@ -16459,7 +16460,7 @@ ${CHUNK_HELP}`);
|
|
|
16459
16460
|
}
|
|
16460
16461
|
|
|
16461
16462
|
// src/compress/index.ts
|
|
16462
|
-
import { existsSync as existsSync9, readdirSync as
|
|
16463
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
16463
16464
|
import { basename as basename8, dirname as dirname8, extname as extname4, join as join22 } from "node:path";
|
|
16464
16465
|
|
|
16465
16466
|
// src/compress/formats.ts
|
|
@@ -17091,7 +17092,7 @@ Exit codes: 0 when all files are done, 1 on an error. An unknown option, an opti
|
|
|
17091
17092
|
a value, a missing input file and two inputs without --batch are errors. No file is written.
|
|
17092
17093
|
`;
|
|
17093
17094
|
var defaultDeps = {
|
|
17094
|
-
readdir: (dir) =>
|
|
17095
|
+
readdir: (dir) => readdirSync3(dir, { withFileTypes: true })
|
|
17095
17096
|
};
|
|
17096
17097
|
function parseArgs2(args2) {
|
|
17097
17098
|
const o = {
|
|
@@ -17454,244 +17455,1776 @@ async function run4(argv, io, deps = defaultDeps) {
|
|
|
17454
17455
|
}
|
|
17455
17456
|
}
|
|
17456
17457
|
|
|
17457
|
-
// src/
|
|
17458
|
-
import {
|
|
17459
|
-
import { resolve as resolve8 } from "node:path";
|
|
17458
|
+
// src/docs/index.ts
|
|
17459
|
+
import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
17460
|
+
import { basename as basename9, isAbsolute as isAbsolute4, join as join26, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
|
|
17460
17461
|
|
|
17461
|
-
// src/
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
atMost(1);
|
|
17497
|
-
return { name: name2, symbol: a };
|
|
17498
|
-
case "is-public":
|
|
17499
|
-
need(2);
|
|
17500
|
-
atMost(2);
|
|
17501
|
-
return { name: name2, pkg: a, symbol: b };
|
|
17502
|
-
case "node-safety":
|
|
17503
|
-
atMost(1);
|
|
17504
|
-
return a === undefined ? { name: name2 } : { name: name2, pkg: a };
|
|
17505
|
-
default:
|
|
17506
|
-
atMost(0);
|
|
17507
|
-
return { name: "cycles" };
|
|
17462
|
+
// src/docs/analyse.ts
|
|
17463
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
17464
|
+
import { join as join24 } from "node:path";
|
|
17465
|
+
|
|
17466
|
+
// src/docs/discovery.ts
|
|
17467
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
17468
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
|
|
17469
|
+
import { join as join23 } from "node:path";
|
|
17470
|
+
var PYTHON_SUFFIXES2 = new Set([".py"]);
|
|
17471
|
+
var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
17472
|
+
var SKIP_DIRS2 = new Set([
|
|
17473
|
+
"node_modules",
|
|
17474
|
+
".git",
|
|
17475
|
+
"dist",
|
|
17476
|
+
"build",
|
|
17477
|
+
"bundle",
|
|
17478
|
+
"out",
|
|
17479
|
+
"coverage",
|
|
17480
|
+
"__pycache__",
|
|
17481
|
+
".venv",
|
|
17482
|
+
"venv",
|
|
17483
|
+
".mypy_cache",
|
|
17484
|
+
".pytest_cache",
|
|
17485
|
+
"vendor",
|
|
17486
|
+
".next",
|
|
17487
|
+
".turbo",
|
|
17488
|
+
"target"
|
|
17489
|
+
]);
|
|
17490
|
+
var SKIP_SUFFIXES = [".d.ts", ".min.js", ".bundle.js", "_pb2.py", ".g.ts"];
|
|
17491
|
+
var CONFIG_NAME = ".code-docs.json";
|
|
17492
|
+
|
|
17493
|
+
class ConfigError extends Error {
|
|
17494
|
+
constructor(message) {
|
|
17495
|
+
super(message);
|
|
17496
|
+
this.name = "ConfigError";
|
|
17508
17497
|
}
|
|
17509
17498
|
}
|
|
17510
|
-
function
|
|
17511
|
-
const
|
|
17512
|
-
if (
|
|
17513
|
-
|
|
17514
|
-
|
|
17499
|
+
function languageOf(path) {
|
|
17500
|
+
const suffix = suffixOf(path.slice(path.lastIndexOf("/") + 1));
|
|
17501
|
+
if (PYTHON_SUFFIXES2.has(suffix))
|
|
17502
|
+
return "python";
|
|
17503
|
+
if (TYPESCRIPT_SUFFIXES.has(suffix))
|
|
17504
|
+
return "typescript";
|
|
17505
|
+
return "";
|
|
17515
17506
|
}
|
|
17516
|
-
function
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
|
|
17541
|
-
throw new Error(`flag ${name2} takes no value`);
|
|
17542
|
-
if (mode !== undefined)
|
|
17543
|
-
throw new Error("use one command or mode in a run");
|
|
17544
|
-
mode = modeCommand;
|
|
17545
|
-
continue;
|
|
17546
|
-
}
|
|
17547
|
-
if (!VALUE_FLAGS2.has(name2)) {
|
|
17548
|
-
throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
|
|
17549
|
-
}
|
|
17550
|
-
if (value === undefined || value === "") {
|
|
17551
|
-
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
17552
|
-
}
|
|
17553
|
-
if (name2 === "--root") {
|
|
17554
|
-
if (root !== undefined)
|
|
17555
|
-
throw new Error("the root is set twice");
|
|
17556
|
-
root = value;
|
|
17557
|
-
} else if (name2 === "--config") {
|
|
17558
|
-
if (isAbsolutePath(value))
|
|
17559
|
-
throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
|
|
17560
|
-
config = value;
|
|
17561
|
-
} else if (name2 === "--out") {
|
|
17562
|
-
if (isAbsolutePath(value))
|
|
17563
|
-
throw new Error(`flag --out ${ABSOLUTE_PATH_TEXT}`);
|
|
17564
|
-
out2 = value;
|
|
17565
|
-
} else {
|
|
17566
|
-
nodeRuntimes = list2(name2, value);
|
|
17567
|
-
}
|
|
17568
|
-
}
|
|
17569
|
-
const [first, ...rest] = words;
|
|
17570
|
-
let command;
|
|
17571
|
-
if (mode !== undefined) {
|
|
17572
|
-
if (first !== undefined) {
|
|
17573
|
-
if (COMMAND_USAGE[first] !== undefined)
|
|
17574
|
-
throw new Error("use one command or mode in a run");
|
|
17575
|
-
throw new Error(`unknown command '${first}' (see repo-tools query --help)`);
|
|
17507
|
+
function isSource(path) {
|
|
17508
|
+
if (path.split("/").some((part) => SKIP_DIRS2.has(part)))
|
|
17509
|
+
return false;
|
|
17510
|
+
if (SKIP_SUFFIXES.some((suffix) => path.endsWith(suffix)))
|
|
17511
|
+
return false;
|
|
17512
|
+
return languageOf(path) !== "";
|
|
17513
|
+
}
|
|
17514
|
+
function gitFiles(root, args2) {
|
|
17515
|
+
const run2 = spawnSync2("git", ["-C", root, "ls-files", "-z", ...args2], {
|
|
17516
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
17517
|
+
timeout: 120000,
|
|
17518
|
+
maxBuffer: 1 << 30
|
|
17519
|
+
});
|
|
17520
|
+
if (run2.error || run2.status !== 0 || !run2.stdout)
|
|
17521
|
+
return null;
|
|
17522
|
+
return new TextDecoder("utf-8").decode(run2.stdout).split("\x00").filter(Boolean);
|
|
17523
|
+
}
|
|
17524
|
+
function walkSource(root) {
|
|
17525
|
+
const found = [];
|
|
17526
|
+
const go = (dir, prefix) => {
|
|
17527
|
+
let entries;
|
|
17528
|
+
try {
|
|
17529
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
17530
|
+
} catch {
|
|
17531
|
+
return;
|
|
17576
17532
|
}
|
|
17577
|
-
|
|
17578
|
-
|
|
17579
|
-
|
|
17580
|
-
|
|
17533
|
+
for (const entry of entries) {
|
|
17534
|
+
const rel = `${prefix}${entry.name}`;
|
|
17535
|
+
if (entry.isDirectory()) {
|
|
17536
|
+
if (!SKIP_DIRS2.has(entry.name))
|
|
17537
|
+
go(join23(dir, entry.name), `${rel}/`);
|
|
17538
|
+
} else if (isSource(rel) && statOk(join23(dir, entry.name))) {
|
|
17539
|
+
found.push(rel);
|
|
17540
|
+
}
|
|
17581
17541
|
}
|
|
17582
|
-
command = makeCommand(first, rest);
|
|
17583
|
-
}
|
|
17584
|
-
if (components) {
|
|
17585
|
-
if (command.name !== "cycles")
|
|
17586
|
-
throw new Error("flag --components applies to cycles only");
|
|
17587
|
-
command = { name: "cycles", components: true };
|
|
17588
|
-
}
|
|
17589
|
-
return {
|
|
17590
|
-
command,
|
|
17591
|
-
root: root ?? cwd,
|
|
17592
|
-
...config === undefined ? {} : { config },
|
|
17593
|
-
...out2 === undefined ? {} : { out: out2 },
|
|
17594
|
-
...nodeRuntimes === undefined ? {} : { nodeRuntimes }
|
|
17595
17542
|
};
|
|
17543
|
+
go(root, "");
|
|
17544
|
+
return found;
|
|
17596
17545
|
}
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
const data = JSON.parse(readFileSync21(graphPath, "utf8"));
|
|
17603
|
-
if (data === null || typeof data !== "object" || !("modules" in data)) {
|
|
17604
|
-
throw new Error(`${graphPath}: no 'modules' key found -- this is not a map dependency-graph.json, or it is ` + "from an incompatible schema. Cannot answer any query against it.");
|
|
17605
|
-
}
|
|
17606
|
-
const warnings = [];
|
|
17607
|
-
const version = data.metadata?.schemaVersion;
|
|
17608
|
-
if (version === undefined || version === null) {
|
|
17609
|
-
warnings.push(`${graphPath}: metadata.schemaVersion is missing -- proceeding as if it matches this ` + `module's schema (${SCHEMA_VERSION}), but an older or newer artifact whose shape has ` + "actually changed may produce a silently wrong answer.");
|
|
17610
|
-
} else if (Number(String(version).split(".")[0]) !== SUPPORTED_SCHEMA_MAJOR) {
|
|
17611
|
-
throw new Error(`${graphPath}: schemaVersion '${String(version)}' is not compatible with this module's ` + `supported major version ${SUPPORTED_SCHEMA_MAJOR} (SCHEMA_VERSION='${SCHEMA_VERSION}'). ` + "Refusing to query a schema this module was not written against rather than risk a " + "silently wrong answer.");
|
|
17546
|
+
function statOk(path) {
|
|
17547
|
+
try {
|
|
17548
|
+
return statSync9(path).isFile();
|
|
17549
|
+
} catch {
|
|
17550
|
+
return false;
|
|
17612
17551
|
}
|
|
17613
|
-
return { data, warnings };
|
|
17614
17552
|
}
|
|
17615
|
-
function
|
|
17616
|
-
|
|
17617
|
-
|
|
17553
|
+
function loadExclusions(root) {
|
|
17554
|
+
const file = join23(root, CONFIG_NAME);
|
|
17555
|
+
if (!existsSync10(file) || !statOk(file))
|
|
17556
|
+
return [];
|
|
17557
|
+
let data;
|
|
17558
|
+
try {
|
|
17559
|
+
data = JSON.parse(readFileSync21(file, "utf8"));
|
|
17560
|
+
} catch (error) {
|
|
17561
|
+
throw new ConfigError(`${CONFIG_NAME}: invalid JSON (${error.message})`);
|
|
17618
17562
|
}
|
|
17619
|
-
|
|
17620
|
-
}
|
|
17621
|
-
var sorted = (items) => items.sort(compareCodeUnits);
|
|
17622
|
-
function dependents(data, file) {
|
|
17623
|
-
const all = entries2(data);
|
|
17624
|
-
if (!all.some(([path]) => path === file)) {
|
|
17625
|
-
throw new Error(`'${file}' is not a file in this graph -- cannot answer 'dependents' for an unknown path. ` + "Check the path is repo-relative POSIX and this is the graph for the right repo/checkout. " + "(A file that IS known but genuinely has no importers returns [] normally, not an error.)");
|
|
17563
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
17564
|
+
throw new ConfigError(`${CONFIG_NAME}: the top level must be an object`);
|
|
17626
17565
|
}
|
|
17627
|
-
|
|
17566
|
+
const list = data.exclude ?? [];
|
|
17567
|
+
if (!Array.isArray(list))
|
|
17568
|
+
throw new ConfigError(`${CONFIG_NAME}: 'exclude' must be an array`);
|
|
17569
|
+
return list.map((entry, i2) => {
|
|
17570
|
+
const record = typeof entry === "object" && entry !== null ? entry : {};
|
|
17571
|
+
const { path, reason } = record;
|
|
17572
|
+
if (typeof path !== "string" || path === "") {
|
|
17573
|
+
throw new ConfigError(`${CONFIG_NAME}: exclude[${i2}] needs a string 'path'`);
|
|
17574
|
+
}
|
|
17575
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
17576
|
+
throw new ConfigError(`${CONFIG_NAME}: exclude[${i2}] (${path}) needs a non-empty 'reason'`);
|
|
17577
|
+
}
|
|
17578
|
+
return {
|
|
17579
|
+
prefix: `${path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")}/`,
|
|
17580
|
+
reason: reason.trim()
|
|
17581
|
+
};
|
|
17582
|
+
});
|
|
17628
17583
|
}
|
|
17629
|
-
function
|
|
17630
|
-
|
|
17584
|
+
function applyExclusions(files, how, root) {
|
|
17585
|
+
const exclusions = loadExclusions(root);
|
|
17586
|
+
if (exclusions.length === 0)
|
|
17587
|
+
return { files, provenance: how };
|
|
17588
|
+
const kept = files.filter((f) => !exclusions.some((e) => `${f}/`.startsWith(e.prefix)));
|
|
17589
|
+
const named = exclusions.map((e) => `${e.prefix} (${e.reason})`).join("; ");
|
|
17590
|
+
return {
|
|
17591
|
+
files: kept,
|
|
17592
|
+
provenance: `${how} -- ${files.length - kept.length} file(s) excluded by ${CONFIG_NAME}: ${named}`
|
|
17593
|
+
};
|
|
17631
17594
|
}
|
|
17632
|
-
function
|
|
17633
|
-
const
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
const targets = (entry.internalDependencies ?? []).map((d) => d.file).filter((f) => f !== undefined && known.has(f));
|
|
17638
|
-
edges.set(path, [...new Set(targets)]);
|
|
17595
|
+
function discover2(root) {
|
|
17596
|
+
const tracked = gitFiles(root, []);
|
|
17597
|
+
if (tracked === null) {
|
|
17598
|
+
const files = sortCodeUnits(walkSource(root));
|
|
17599
|
+
return applyExclusions(files, "filesystem walk (NOT a git repo -- may include generated files)", root);
|
|
17639
17600
|
}
|
|
17640
|
-
const
|
|
17641
|
-
|
|
17642
|
-
|
|
17601
|
+
const files = sortCodeUnits(tracked.filter(isSource));
|
|
17602
|
+
const untracked = (gitFiles(root, ["--others", "--exclude-standard"]) ?? []).filter(isSource);
|
|
17603
|
+
if (untracked.length > 0 && files.length === 0) {
|
|
17604
|
+
return applyExclusions(sortCodeUnits(untracked), `filesystem walk -- git tracks NO source here, ${untracked.length} untracked file(s) found; ` + "commit them or the gate measures nothing", root);
|
|
17643
17605
|
}
|
|
17644
|
-
|
|
17645
|
-
}
|
|
17646
|
-
|
|
17647
|
-
// src/query/graph.ts
|
|
17648
|
-
function fileEntriesOf(graph) {
|
|
17649
|
-
const pairs = [];
|
|
17650
|
-
for (const files of Object.values(graph.modules)) {
|
|
17651
|
-
for (const [file, entry] of Object.entries(files))
|
|
17652
|
-
pairs.push([file, entry]);
|
|
17606
|
+
if (untracked.length > 0) {
|
|
17607
|
+
return applyExclusions(files, `git ls-files (${files.length} tracked) -- WARNING: ${untracked.length} untracked source ` + "file(s) were NOT measured", root);
|
|
17653
17608
|
}
|
|
17654
|
-
return
|
|
17609
|
+
return applyExclusions(files, "git ls-files", root);
|
|
17655
17610
|
}
|
|
17656
|
-
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
if (dep.file !== undefined && allFiles.has(dep.file))
|
|
17662
|
-
targets.add(dep.file);
|
|
17663
|
-
}
|
|
17664
|
-
forward.set(file, targets);
|
|
17665
|
-
}
|
|
17666
|
-
return forward;
|
|
17611
|
+
|
|
17612
|
+
// src/docs/model.ts
|
|
17613
|
+
var STUB_MARKER = "TODO:";
|
|
17614
|
+
function isParsed(report) {
|
|
17615
|
+
return report.error === "";
|
|
17667
17616
|
}
|
|
17668
|
-
function
|
|
17669
|
-
|
|
17670
|
-
for (const [file, targets] of forward) {
|
|
17671
|
-
for (const target of targets) {
|
|
17672
|
-
const importers = reverse.get(target) ?? new Set;
|
|
17673
|
-
importers.add(file);
|
|
17674
|
-
reverse.set(target, importers);
|
|
17675
|
-
}
|
|
17676
|
-
}
|
|
17677
|
-
const out2 = {};
|
|
17678
|
-
for (const target of sortCodeUnits([...reverse.keys()])) {
|
|
17679
|
-
out2[target] = sortCodeUnits([...reverse.get(target) ?? []]);
|
|
17680
|
-
}
|
|
17681
|
-
return out2;
|
|
17617
|
+
function newReport(path, language) {
|
|
17618
|
+
return { path, language, symbols: [], dialects: new Set, error: "" };
|
|
17682
17619
|
}
|
|
17683
17620
|
|
|
17684
|
-
// src/
|
|
17685
|
-
|
|
17686
|
-
var
|
|
17687
|
-
|
|
17688
|
-
|
|
17621
|
+
// src/ste/rules.ts
|
|
17622
|
+
var IRREGULAR_PARTICIPLES = "read|built|sent|held|kept|made|found|put|set|run|lost|left|met|paid|told" + "|thrown|drawn|given|taken|shown|known|grown|blown" + "|cut|meant|dealt|felt|sold|said|spent|split|shut|hit|let|bred|fed|led";
|
|
17623
|
+
var DETERMINERS = "the|a|an|its|their|this|that|our|your|each|every" + "|his|her|my|these|those";
|
|
17624
|
+
var PASSIVE_SOURCE = `${B}(?:${ci("is|are|was|were|be|been|being")})${S}+` + `(?:${W}+(?:${ci("ed|en")})|(?:${ci(IRREGULAR_PARTICIPLES)}))${S}+` + `by${S}+(?:(?:${ci(DETERMINERS)})${B}|[A-Z\`])`;
|
|
17625
|
+
var AMBIGUOUS_VERBS = "is|are|was|were|be|will|can|may|must|does|do|has|have|should|shall" + "|disables?|enables?|causes?|returns?|means?|allows?|prevents?|makes?" + "|sets?|gets?|adds?|removes?|creates?|requires?|affects?|applies|happens?";
|
|
17626
|
+
var AMBIGUOUS_SOURCE = `(?:^|(?<=[.!?,;:])${S})${S}*(?:(?:${ci("and|but|or|so|then|yet")})${S}+)?` + `(${ci("it|this|that|these|those")})${S}+(?:${AMBIGUOUS_VERBS})${B}`;
|
|
17627
|
+
var WORDY = [
|
|
17628
|
+
["in order to", "to"],
|
|
17629
|
+
["at this point in time", "now"],
|
|
17630
|
+
["in the event that", "if"],
|
|
17631
|
+
["due to the fact that", "because"],
|
|
17632
|
+
["a number of", "some"],
|
|
17633
|
+
["utilise", "use"],
|
|
17634
|
+
["utilize", "use"],
|
|
17635
|
+
["prior to", "before"],
|
|
17636
|
+
["subsequent to", "after"],
|
|
17637
|
+
["in the case of", "for"],
|
|
17638
|
+
["in spite of the fact that", "although"],
|
|
17639
|
+
["for the purpose of", "to"],
|
|
17640
|
+
["has the ability to", "can"],
|
|
17641
|
+
["is able to", "can"]
|
|
17642
|
+
];
|
|
17643
|
+
var IDENTIFIER_SOURCE = "`[^`]*`" + `|${B}${W}+\\(\\)` + `|${B}${W}*[a-z]${W}*[A-Z]${W}*${B}` + `|${B}${W}+_${W}+${B}` + `|${B}${W}+\\.${W}+${B}`;
|
|
17644
|
+
var MAX_WORDS_PROCEDURAL = 20;
|
|
17645
|
+
var MAX_WORDS_DESCRIPTIVE = 25;
|
|
17646
|
+
function rule(source, flags2 = "") {
|
|
17647
|
+
return new RegExp(source, `u${flags2}`);
|
|
17648
|
+
}
|
|
17649
|
+
function stripIdentifiers(text) {
|
|
17650
|
+
return text.replace(rule(IDENTIFIER_SOURCE, "g"), " ");
|
|
17651
|
+
}
|
|
17652
|
+
|
|
17653
|
+
// src/ste/prose.ts
|
|
17654
|
+
var PASSIVE = rule(PASSIVE_SOURCE);
|
|
17655
|
+
var AMBIGUOUS = rule(AMBIGUOUS_SOURCE);
|
|
17656
|
+
var SENTENCE_END = rule(`(?<=[.!?])${S}+`);
|
|
17657
|
+
function sentencesOf(text) {
|
|
17658
|
+
const collapsed = pySplit(text).join(" ");
|
|
17659
|
+
return collapsed.split(SENTENCE_END).map(pyStrip).filter((s) => s !== "");
|
|
17660
|
+
}
|
|
17661
|
+
function clip(text, limit = 60) {
|
|
17662
|
+
return codePointLength(text) <= limit ? text : `${headCodePoints(text, limit - 1)}…`;
|
|
17663
|
+
}
|
|
17664
|
+
function checkProse(text) {
|
|
17665
|
+
const findings = [];
|
|
17666
|
+
for (const sentence of sentencesOf(text)) {
|
|
17667
|
+
const words = pySplit(sentence);
|
|
17668
|
+
if (words.length > MAX_WORDS_PROCEDURAL) {
|
|
17669
|
+
findings.push([
|
|
17670
|
+
"STE-LEN",
|
|
17671
|
+
`sentence has ${words.length} words (limit ${MAX_WORDS_PROCEDURAL}): '${clip(sentence)}'`
|
|
17672
|
+
]);
|
|
17673
|
+
}
|
|
17674
|
+
const passive = PASSIVE.exec(sentence);
|
|
17675
|
+
if (passive) {
|
|
17676
|
+
findings.push(["STE-VOICE", `possible passive voice '${passive[0]}' -- prefer active`]);
|
|
17677
|
+
}
|
|
17678
|
+
const ambiguous = AMBIGUOUS.exec(sentence);
|
|
17679
|
+
if (ambiguous) {
|
|
17680
|
+
findings.push(["STE-REF", `ambiguous '${ambiguous[1]}' -- name the thing it refers to`]);
|
|
17681
|
+
}
|
|
17682
|
+
}
|
|
17683
|
+
const prose = stripIdentifiers(text);
|
|
17684
|
+
for (const [phrase, better] of WORDY) {
|
|
17685
|
+
if (rule(`${B}${ci(reEscape(phrase))}${B}`).test(prose)) {
|
|
17686
|
+
findings.push(["STE-WORD", `'${phrase}' -- prefer '${better}'`]);
|
|
17687
|
+
}
|
|
17688
|
+
}
|
|
17689
|
+
return findings;
|
|
17690
|
+
}
|
|
17691
|
+
var SECTION_HEADER = /^(Args|Arguments|Returns|Raises|Yields|Examples|Attributes|Parameters|Notes?|See Also):?$/u;
|
|
17692
|
+
function proseOf(doc) {
|
|
17693
|
+
const out2 = [];
|
|
17694
|
+
let inFence = false;
|
|
17695
|
+
for (const raw of pySplitlines(doc)) {
|
|
17696
|
+
const line = pyStrip(pyStrip(raw).replace(/^\*+/, ""));
|
|
17697
|
+
if (line.startsWith("```")) {
|
|
17698
|
+
inFence = !inFence;
|
|
17699
|
+
continue;
|
|
17700
|
+
}
|
|
17701
|
+
if (inFence || line.startsWith("@") || line.startsWith(">>>"))
|
|
17702
|
+
continue;
|
|
17703
|
+
if (SECTION_HEADER.test(line))
|
|
17704
|
+
continue;
|
|
17705
|
+
if (/^-{3,}$/u.test(line))
|
|
17706
|
+
continue;
|
|
17707
|
+
out2.push(line);
|
|
17708
|
+
}
|
|
17709
|
+
return out2.filter((p) => p !== "").join(" ");
|
|
17710
|
+
}
|
|
17711
|
+
|
|
17712
|
+
// src/docs/rules.ts
|
|
17713
|
+
var TEST_PATH = /(^|\/)(tests?|__tests__|spec)\/|(^|\/)test_[^/]*$|_test\.[^/]+$|\.(test|spec)\.[^/]+$/;
|
|
17714
|
+
function isTestFile2(path) {
|
|
17715
|
+
return TEST_PATH.test(path.replace(/\\/g, "/"));
|
|
17716
|
+
}
|
|
17717
|
+
function applyRules(sym, doc, path, options) {
|
|
17718
|
+
if (sym.exported && !sym.hasDoc && !isTestFile2(path)) {
|
|
17719
|
+
sym.issues.push({ rule: "M1", tier: "MUST", detail: `exported symbol has no ${options.noun}` });
|
|
17720
|
+
return;
|
|
17721
|
+
}
|
|
17722
|
+
if (!sym.hasDoc)
|
|
17723
|
+
return;
|
|
17724
|
+
if (!sym.summary) {
|
|
17725
|
+
sym.issues.push({ rule: "M2", tier: "MUST", detail: `${options.noun} has no summary line` });
|
|
17726
|
+
}
|
|
17727
|
+
for (const p of sym.docParams) {
|
|
17728
|
+
if (!sym.params.includes(p)) {
|
|
17729
|
+
sym.issues.push({
|
|
17730
|
+
rule: "M3",
|
|
17731
|
+
tier: "MUST",
|
|
17732
|
+
detail: `documents parameter '${p}' which is not in the signature`
|
|
17733
|
+
});
|
|
17734
|
+
}
|
|
17735
|
+
}
|
|
17736
|
+
if (doc.includes(STUB_MARKER)) {
|
|
17737
|
+
sym.issues.push({
|
|
17738
|
+
rule: "M4",
|
|
17739
|
+
tier: "MUST",
|
|
17740
|
+
detail: "unfilled stub marker left by `stub`"
|
|
17741
|
+
});
|
|
17742
|
+
}
|
|
17743
|
+
if (sym.summary && !sym.summary.endsWith(".")) {
|
|
17744
|
+
sym.issues.push({ rule: "S2", tier: "SHOULD", detail: "summary does not end with a period" });
|
|
17745
|
+
}
|
|
17746
|
+
if (options.restatesType !== null) {
|
|
17747
|
+
sym.issues.push({ rule: "S5", tier: "SHOULD", detail: options.restatesType });
|
|
17748
|
+
}
|
|
17749
|
+
for (const [rule, detail] of checkProse(options.prose)) {
|
|
17750
|
+
sym.issues.push({ rule: `S6/${rule}`, tier: "SHOULD", detail });
|
|
17751
|
+
}
|
|
17752
|
+
}
|
|
17753
|
+
|
|
17754
|
+
// src/docs/python.ts
|
|
17755
|
+
var GOOGLE = new RegExp(`^${S}*(Args|Arguments|Returns|Raises|Yields):${S}*$`, "mu");
|
|
17756
|
+
var NUMPY = new RegExp(`^${S}*Parameters${S}*\\n${S}*-{3,}${S}*$`, "mu");
|
|
17757
|
+
var REST_TYPE = `(?:[^:\\n]*${S})?`;
|
|
17758
|
+
var REST = new RegExp(`^${S}*:param${S}+${REST_TYPE}\\*{0,2}${W}+${S}*:`, "mu");
|
|
17759
|
+
var GOOGLE_PARAM = new RegExp(`^${S}{1,8}(\\*{0,2}${W}+)${S}*(?:\\([^)]*\\))?${S}*:`, "gmu");
|
|
17760
|
+
var NUMPY_PARAM = new RegExp(`^${S}*(\\*{0,2}${W}+)${S}*:${S}*${NON_SPACE}`, "gmu");
|
|
17761
|
+
var REST_PARAM = new RegExp(`^${S}*:param${S}+${REST_TYPE}(\\*{0,2}${W}+)${S}*:`, "gmu");
|
|
17762
|
+
var SECTION_HEADERS = new Set([
|
|
17763
|
+
"Args",
|
|
17764
|
+
"Arguments",
|
|
17765
|
+
"Returns",
|
|
17766
|
+
"Raises",
|
|
17767
|
+
"Yields",
|
|
17768
|
+
"Examples",
|
|
17769
|
+
"Attributes",
|
|
17770
|
+
"Note",
|
|
17771
|
+
"Notes",
|
|
17772
|
+
"Parameters",
|
|
17773
|
+
"See Also"
|
|
17774
|
+
]);
|
|
17775
|
+
function detectDialect(doc) {
|
|
17776
|
+
if (NUMPY.test(doc))
|
|
17777
|
+
return "numpy";
|
|
17778
|
+
if (GOOGLE.test(doc))
|
|
17779
|
+
return "google";
|
|
17780
|
+
if (REST.test(doc))
|
|
17781
|
+
return "rest";
|
|
17782
|
+
return "";
|
|
17783
|
+
}
|
|
17784
|
+
var isRule = (line) => /^-+$/.test(pyStrip(line));
|
|
17785
|
+
function section2(doc, header) {
|
|
17786
|
+
const lines = doc.split(`
|
|
17787
|
+
`);
|
|
17788
|
+
const out2 = [];
|
|
17789
|
+
let inside = false;
|
|
17790
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
17791
|
+
const stripped = pyStrip(lines[i2] ?? "");
|
|
17792
|
+
const next = lines[i2 + 1];
|
|
17793
|
+
const underlined = next !== undefined && isRule(next);
|
|
17794
|
+
if (!inside) {
|
|
17795
|
+
if (stripped.replace(/:+$/, "") === header && stripped.endsWith(":"))
|
|
17796
|
+
inside = true;
|
|
17797
|
+
else if (stripped === header && underlined)
|
|
17798
|
+
inside = true;
|
|
17799
|
+
continue;
|
|
17800
|
+
}
|
|
17801
|
+
if (SECTION_HEADERS.has(stripped.replace(/:+$/, "")) && (stripped.endsWith(":") || underlined)) {
|
|
17802
|
+
break;
|
|
17803
|
+
}
|
|
17804
|
+
out2.push(lines[i2] ?? "");
|
|
17805
|
+
}
|
|
17806
|
+
return out2.join(`
|
|
17807
|
+
`);
|
|
17808
|
+
}
|
|
17809
|
+
function namesOf(pattern, block) {
|
|
17810
|
+
return [...block.matchAll(pattern)].map((m) => (m[1] ?? "").replace(/^\*+/, ""));
|
|
17811
|
+
}
|
|
17812
|
+
function documentedParams(doc, dialect) {
|
|
17813
|
+
if (dialect === "rest")
|
|
17814
|
+
return namesOf(REST_PARAM, doc);
|
|
17815
|
+
if (dialect === "numpy")
|
|
17816
|
+
return namesOf(NUMPY_PARAM, section2(doc, "Parameters"));
|
|
17817
|
+
if (dialect === "google") {
|
|
17818
|
+
return namesOf(GOOGLE_PARAM, section2(doc, "Args") || section2(doc, "Arguments"));
|
|
17819
|
+
}
|
|
17820
|
+
return [];
|
|
17821
|
+
}
|
|
17822
|
+
function expandTabs(text) {
|
|
17823
|
+
let out2 = "";
|
|
17824
|
+
let column = 0;
|
|
17825
|
+
for (const ch of text) {
|
|
17826
|
+
if (ch === "\t") {
|
|
17827
|
+
const width = 8 - column % 8;
|
|
17828
|
+
out2 += " ".repeat(width);
|
|
17829
|
+
column += width;
|
|
17830
|
+
} else {
|
|
17831
|
+
out2 += ch;
|
|
17832
|
+
column = ch === `
|
|
17833
|
+
` || ch === "\r" ? 0 : column + 1;
|
|
17834
|
+
}
|
|
17835
|
+
}
|
|
17836
|
+
return out2;
|
|
17837
|
+
}
|
|
17838
|
+
function cleandoc(doc) {
|
|
17839
|
+
const lines = expandTabs(doc).split(`
|
|
17840
|
+
`);
|
|
17841
|
+
const stripSpaces = (line) => line.replace(/^ +/, "");
|
|
17842
|
+
let margin = Number.POSITIVE_INFINITY;
|
|
17843
|
+
for (const line of lines.slice(1)) {
|
|
17844
|
+
const content = stripSpaces(line).length;
|
|
17845
|
+
if (content > 0)
|
|
17846
|
+
margin = Math.min(margin, line.length - content);
|
|
17847
|
+
}
|
|
17848
|
+
lines[0] = stripSpaces(lines[0] ?? "");
|
|
17849
|
+
if (margin < Number.POSITIVE_INFINITY) {
|
|
17850
|
+
for (let i2 = 1;i2 < lines.length; i2++)
|
|
17851
|
+
lines[i2] = (lines[i2] ?? "").slice(margin);
|
|
17852
|
+
}
|
|
17853
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
17854
|
+
lines.pop();
|
|
17855
|
+
while (lines.length > 0 && lines[0] === "")
|
|
17856
|
+
lines.shift();
|
|
17857
|
+
return lines.join(`
|
|
17858
|
+
`);
|
|
17859
|
+
}
|
|
17860
|
+
var ESCAPES = {
|
|
17861
|
+
"\\": "\\",
|
|
17862
|
+
"'": "'",
|
|
17863
|
+
'"': '"',
|
|
17864
|
+
a: "\x07",
|
|
17865
|
+
b: "\b",
|
|
17866
|
+
f: "\f",
|
|
17867
|
+
n: `
|
|
17868
|
+
`,
|
|
17869
|
+
r: "\r",
|
|
17870
|
+
t: "\t",
|
|
17871
|
+
v: "\v"
|
|
17872
|
+
};
|
|
17873
|
+
function decodeEscapes(body2) {
|
|
17874
|
+
return body2.replace(/\\(\r\n|[\n\r]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-7]{1,3}|N\{[^}]*\}|[\s\S])/g, (whole, seq) => {
|
|
17875
|
+
if (seq === `
|
|
17876
|
+
` || seq === "\r" || seq === `\r
|
|
17877
|
+
`)
|
|
17878
|
+
return "";
|
|
17879
|
+
const simple = ESCAPES[seq];
|
|
17880
|
+
if (simple !== undefined)
|
|
17881
|
+
return simple;
|
|
17882
|
+
if (/^[0-7]+$/.test(seq))
|
|
17883
|
+
return String.fromCharCode(Number.parseInt(seq, 8));
|
|
17884
|
+
if (/^[xuU][0-9a-fA-F]+$/.test(seq)) {
|
|
17885
|
+
const code = Number.parseInt(seq.slice(1), 16);
|
|
17886
|
+
return code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
17887
|
+
}
|
|
17888
|
+
return whole;
|
|
17889
|
+
});
|
|
17890
|
+
}
|
|
17891
|
+
function stringValue(node) {
|
|
17892
|
+
const m = /^([A-Za-z]*)('''|"""|'|")/.exec(node.text);
|
|
17893
|
+
if (!m)
|
|
17894
|
+
return null;
|
|
17895
|
+
const prefix = (m[1] ?? "").toLowerCase();
|
|
17896
|
+
const quote = m[2] ?? "";
|
|
17897
|
+
if (/[bft]/.test(prefix))
|
|
17898
|
+
return null;
|
|
17899
|
+
const body2 = node.text.slice(prefix.length + quote.length, node.text.length - quote.length);
|
|
17900
|
+
return prefix.includes("r") ? body2 : decodeEscapes(body2);
|
|
17901
|
+
}
|
|
17902
|
+
function constantString(node) {
|
|
17903
|
+
if (node.type === "string")
|
|
17904
|
+
return stringValue(node);
|
|
17905
|
+
if (node.type === "concatenated_string") {
|
|
17906
|
+
let joined = "";
|
|
17907
|
+
for (const part of node.namedChildren) {
|
|
17908
|
+
if (part.type !== "string")
|
|
17909
|
+
return null;
|
|
17910
|
+
const value = stringValue(part);
|
|
17911
|
+
if (value === null)
|
|
17912
|
+
return null;
|
|
17913
|
+
joined += value;
|
|
17914
|
+
}
|
|
17915
|
+
return joined;
|
|
17916
|
+
}
|
|
17917
|
+
if (node.type === "parenthesized_expression" && node.namedChildren.length === 1) {
|
|
17918
|
+
const inner = node.namedChildren[0];
|
|
17919
|
+
return inner ? constantString(inner) : null;
|
|
17920
|
+
}
|
|
17921
|
+
return null;
|
|
17922
|
+
}
|
|
17923
|
+
function docstringOf(def) {
|
|
17924
|
+
const body2 = def.childForFieldName("body");
|
|
17925
|
+
const first = body2?.namedChildren.find((c) => c.type !== "comment");
|
|
17926
|
+
if (first?.type !== "expression_statement")
|
|
17927
|
+
return "";
|
|
17928
|
+
const value = first.namedChildren[0];
|
|
17929
|
+
if (!value || first.namedChildren.length !== 1)
|
|
17930
|
+
return "";
|
|
17931
|
+
const text = constantString(value);
|
|
17932
|
+
return text === null ? "" : cleandoc(text);
|
|
17933
|
+
}
|
|
17934
|
+
function parameterName(param) {
|
|
17935
|
+
switch (param.type) {
|
|
17936
|
+
case "identifier":
|
|
17937
|
+
return { name: param.text, star: "" };
|
|
17938
|
+
case "typed_parameter": {
|
|
17939
|
+
const first = param.namedChildren[0];
|
|
17940
|
+
return first ? parameterName(first) : null;
|
|
17941
|
+
}
|
|
17942
|
+
case "default_parameter":
|
|
17943
|
+
case "typed_default_parameter": {
|
|
17944
|
+
const name2 = param.childForFieldName("name");
|
|
17945
|
+
return name2?.type === "identifier" ? { name: name2.text, star: "" } : null;
|
|
17946
|
+
}
|
|
17947
|
+
case "list_splat_pattern":
|
|
17948
|
+
case "dictionary_splat_pattern": {
|
|
17949
|
+
const inner = param.namedChildren.find((c) => c.type === "identifier");
|
|
17950
|
+
return inner ? { name: inner.text, star: param.type === "list_splat_pattern" ? "*" : "**" } : null;
|
|
17951
|
+
}
|
|
17952
|
+
default:
|
|
17953
|
+
return null;
|
|
17954
|
+
}
|
|
17955
|
+
}
|
|
17956
|
+
function paramsOf(def) {
|
|
17957
|
+
const list = def.childForFieldName("parameters");
|
|
17958
|
+
const plain = [];
|
|
17959
|
+
let vararg;
|
|
17960
|
+
let kwarg;
|
|
17961
|
+
for (const param of list?.namedChildren ?? []) {
|
|
17962
|
+
const found = parameterName(param);
|
|
17963
|
+
if (!found)
|
|
17964
|
+
continue;
|
|
17965
|
+
if (found.star === "*")
|
|
17966
|
+
vararg = found.name;
|
|
17967
|
+
else if (found.star === "**")
|
|
17968
|
+
kwarg = found.name;
|
|
17969
|
+
else
|
|
17970
|
+
plain.push(found.name);
|
|
17971
|
+
}
|
|
17972
|
+
const names = [...plain, ...vararg ? [vararg] : [], ...kwarg ? [kwarg] : []];
|
|
17973
|
+
return names.filter((n) => n !== "self" && n !== "cls");
|
|
17974
|
+
}
|
|
17975
|
+
function isExported(name2, moduleAll) {
|
|
17976
|
+
if (moduleAll !== null)
|
|
17977
|
+
return moduleAll.has(name2);
|
|
17978
|
+
if (name2.startsWith("__") && name2.endsWith("__"))
|
|
17979
|
+
return true;
|
|
17980
|
+
return !name2.startsWith("_");
|
|
17981
|
+
}
|
|
17982
|
+
function literalNames(value) {
|
|
17983
|
+
if (!["list", "tuple", "set"].includes(value.type))
|
|
17984
|
+
return null;
|
|
17985
|
+
const names = new Set;
|
|
17986
|
+
for (const item of value.namedChildren) {
|
|
17987
|
+
if (item.type === "comment")
|
|
17988
|
+
continue;
|
|
17989
|
+
const text = constantString(item);
|
|
17990
|
+
if (text === null)
|
|
17991
|
+
return null;
|
|
17992
|
+
names.add(text);
|
|
17993
|
+
}
|
|
17994
|
+
return names;
|
|
17995
|
+
}
|
|
17996
|
+
function moduleAllOf(root) {
|
|
17997
|
+
let found = null;
|
|
17998
|
+
for (const stmt of root.namedChildren) {
|
|
17999
|
+
const assign = stmt.type === "expression_statement" ? stmt.namedChildren[0] : undefined;
|
|
18000
|
+
if (assign?.type !== "assignment" || assign.childForFieldName("type"))
|
|
18001
|
+
continue;
|
|
18002
|
+
const left = assign.childForFieldName("left");
|
|
18003
|
+
const right = assign.childForFieldName("right");
|
|
18004
|
+
if (left?.type !== "identifier" || left.text !== "__all__" || !right)
|
|
18005
|
+
continue;
|
|
18006
|
+
found = literalNames(right);
|
|
18007
|
+
}
|
|
18008
|
+
return found;
|
|
18009
|
+
}
|
|
18010
|
+
function firstErrorLine(root) {
|
|
18011
|
+
let node = root;
|
|
18012
|
+
for (;; ) {
|
|
18013
|
+
const inner = node.children.find((c) => c.hasError || c.isMissing);
|
|
18014
|
+
if (!inner)
|
|
18015
|
+
return node.startPosition.row + 1;
|
|
18016
|
+
node = inner;
|
|
18017
|
+
}
|
|
18018
|
+
}
|
|
18019
|
+
var PYTHON_2_STATEMENTS = ["print_statement", "exec_statement"];
|
|
18020
|
+
function legacyStatement(root) {
|
|
18021
|
+
return root.descendantsOfType(PYTHON_2_STATEMENTS).find((n) => !n.namedChildren.some((c) => c.type === "chevron"));
|
|
18022
|
+
}
|
|
18023
|
+
function indentationError(root) {
|
|
18024
|
+
for (const scope of [root, ...root.descendantsOfType(["block"])]) {
|
|
18025
|
+
const isBlock = scope.type === "block";
|
|
18026
|
+
const statements = scope.namedChildren.filter((c) => c.type !== "comment");
|
|
18027
|
+
if (isBlock && statements.length === 0) {
|
|
18028
|
+
return `IndentationError: expected an indented block at line ${scope.startPosition.row + 2}`;
|
|
18029
|
+
}
|
|
18030
|
+
const inline = isBlock && scope.previousSibling?.endPosition.row === scope.startPosition.row;
|
|
18031
|
+
let expected = isBlock ? -1 : 0;
|
|
18032
|
+
let previousEnd = inline ? scope.startPosition.row : -1;
|
|
18033
|
+
for (const stmt of statements) {
|
|
18034
|
+
const { row, column } = stmt.startPosition;
|
|
18035
|
+
if (row !== previousEnd) {
|
|
18036
|
+
if (expected < 0)
|
|
18037
|
+
expected = column;
|
|
18038
|
+
else if (column !== expected) {
|
|
18039
|
+
const kind = isBlock ? "inconsistent indentation" : "unexpected indent";
|
|
18040
|
+
return `IndentationError: ${kind} at line ${row + 1}`;
|
|
18041
|
+
}
|
|
18042
|
+
}
|
|
18043
|
+
previousEnd = stmt.endPosition.row;
|
|
18044
|
+
}
|
|
18045
|
+
}
|
|
18046
|
+
return null;
|
|
18047
|
+
}
|
|
18048
|
+
var LEADING_ZERO = /^0[0-9_]*[1-9][0-9_]*$/;
|
|
18049
|
+
function syntaxError(root) {
|
|
18050
|
+
if (root.hasError)
|
|
18051
|
+
return `SyntaxError: invalid syntax at line ${firstErrorLine(root)}`;
|
|
18052
|
+
const legacy = legacyStatement(root);
|
|
18053
|
+
if (legacy)
|
|
18054
|
+
return `SyntaxError: Python 2 statement at line ${legacy.startPosition.row + 1}`;
|
|
18055
|
+
const indent = indentationError(root);
|
|
18056
|
+
if (indent)
|
|
18057
|
+
return indent;
|
|
18058
|
+
const octal = root.descendantsOfType(["integer"]).find((n) => LEADING_ZERO.test(n.text));
|
|
18059
|
+
if (octal) {
|
|
18060
|
+
const line = octal.startPosition.row + 1;
|
|
18061
|
+
return `SyntaxError: leading zeros in decimal integer literals are not permitted at line ${line}`;
|
|
18062
|
+
}
|
|
18063
|
+
return null;
|
|
18064
|
+
}
|
|
18065
|
+
function parsesAsPython(text) {
|
|
18066
|
+
const tree = parserFor("python").parse(text.replace(/^\uFEFF/, ""));
|
|
18067
|
+
if (!tree)
|
|
18068
|
+
return false;
|
|
18069
|
+
try {
|
|
18070
|
+
return syntaxError(tree.rootNode) === null;
|
|
18071
|
+
} finally {
|
|
18072
|
+
tree.delete();
|
|
18073
|
+
}
|
|
18074
|
+
}
|
|
18075
|
+
function summaryOf(doc) {
|
|
18076
|
+
return pySplitlines(pyStrip(doc))[0] ?? "";
|
|
18077
|
+
}
|
|
18078
|
+
function restatesTypes(doc) {
|
|
18079
|
+
const header = new RegExp(`^${S}*(Args|Parameters):?${S}*$`, "mu");
|
|
18080
|
+
const typed = new RegExp(`^${S}{1,8}\\*{0,2}${W}+${S}*\\([^)]+\\)${S}*:`, "mu");
|
|
18081
|
+
return header.test(doc) && doc.includes("(") && typed.test(doc);
|
|
18082
|
+
}
|
|
18083
|
+
async function analysePython(path, source) {
|
|
18084
|
+
const report = newReport(path, "python");
|
|
18085
|
+
await loadGrammar("python");
|
|
18086
|
+
const tree = parserFor("python").parse(source);
|
|
18087
|
+
if (!tree) {
|
|
18088
|
+
report.error = "SyntaxError: the parser returned no tree";
|
|
18089
|
+
return report;
|
|
18090
|
+
}
|
|
18091
|
+
try {
|
|
18092
|
+
const root = tree.rootNode;
|
|
18093
|
+
const failure = syntaxError(root);
|
|
18094
|
+
if (failure) {
|
|
18095
|
+
report.error = failure;
|
|
18096
|
+
return report;
|
|
18097
|
+
}
|
|
18098
|
+
const moduleAll = moduleAllOf(root);
|
|
18099
|
+
for (const def of root.descendantsOfType(["function_definition", "class_definition"])) {
|
|
18100
|
+
const doc = docstringOf(def);
|
|
18101
|
+
const dialect = detectDialect(doc);
|
|
18102
|
+
const isClass = def.type === "class_definition";
|
|
18103
|
+
const name2 = def.childForFieldName("name")?.text ?? "<anonymous>";
|
|
18104
|
+
const line = def.startPosition.row + 1;
|
|
18105
|
+
const sym = {
|
|
18106
|
+
file: path,
|
|
18107
|
+
line,
|
|
18108
|
+
anchorLine: line,
|
|
18109
|
+
anchorColumn: 0,
|
|
18110
|
+
name: name2,
|
|
18111
|
+
kind: isClass ? "class" : "function",
|
|
18112
|
+
exported: isExported(name2, moduleAll),
|
|
18113
|
+
hasDoc: pyStrip(doc) !== "",
|
|
18114
|
+
params: isClass ? [] : paramsOf(def),
|
|
18115
|
+
docParams: doc ? documentedParams(doc, dialect) : [],
|
|
18116
|
+
summary: summaryOf(doc),
|
|
18117
|
+
dialect,
|
|
18118
|
+
issues: []
|
|
18119
|
+
};
|
|
18120
|
+
applyRules(sym, doc, path, {
|
|
18121
|
+
noun: "docstring",
|
|
18122
|
+
restatesType: restatesTypes(doc) ? "docstring restates types already in the signature" : null,
|
|
18123
|
+
prose: proseOf(doc)
|
|
18124
|
+
});
|
|
18125
|
+
report.symbols.push(sym);
|
|
18126
|
+
if (dialect)
|
|
18127
|
+
report.dialects.add(dialect);
|
|
18128
|
+
}
|
|
18129
|
+
} finally {
|
|
18130
|
+
tree.delete();
|
|
18131
|
+
}
|
|
18132
|
+
return report;
|
|
18133
|
+
}
|
|
18134
|
+
|
|
18135
|
+
// src/docs/typescript.ts
|
|
18136
|
+
var NAME = "[\\p{L}\\p{N}_$]";
|
|
18137
|
+
var TSDOC_PARAM = new RegExp(`^${S}*\\*?${S}*@param${S}+(?:\\{[^}]*\\}${S}*)?\\[?(${NAME}+)`, "gmu");
|
|
18138
|
+
var JSDOC_TYPED_PARAM = new RegExp(`@param${S}+\\{[^}]+\\}`, "u");
|
|
18139
|
+
var JSDOC_TYPED_TAG = new RegExp(`@(?:param|returns?|type|typedef|property|prop|throws|this)${S}+\\{[^}]+\\}`, "u");
|
|
18140
|
+
var TSDOC_TAG = new RegExp(`@param${S}+${NAME}+${S}+-${S}|@(?:returns?|throws)${S}+(?!\\{)${NON_SPACE}|@(?:remarks|typeParam|defaultValue)${B}`, "u");
|
|
18141
|
+
function dialectOf(doc) {
|
|
18142
|
+
if (!doc)
|
|
18143
|
+
return "";
|
|
18144
|
+
if (JSDOC_TYPED_TAG.test(doc))
|
|
18145
|
+
return "jsdoc";
|
|
18146
|
+
if (TSDOC_TAG.test(doc))
|
|
18147
|
+
return "tsdoc";
|
|
18148
|
+
return "";
|
|
18149
|
+
}
|
|
18150
|
+
var DECLARATIONS = {
|
|
18151
|
+
function_declaration: "function",
|
|
18152
|
+
generator_function_declaration: "function",
|
|
18153
|
+
class_declaration: "class",
|
|
18154
|
+
abstract_class_declaration: "class",
|
|
18155
|
+
interface_declaration: "interface",
|
|
18156
|
+
type_alias_declaration: "type",
|
|
18157
|
+
method_definition: "method"
|
|
18158
|
+
};
|
|
18159
|
+
var DECLARATION_TYPES = Object.keys(DECLARATIONS);
|
|
18160
|
+
function grammarFor(path) {
|
|
18161
|
+
return path.endsWith(".tsx") || path.endsWith(".jsx") ? "tsx" : "typescript";
|
|
18162
|
+
}
|
|
18163
|
+
var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
|
|
18164
|
+
var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
|
|
18165
|
+
var EXPORT_TYPE_STAR = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
|
|
18166
|
+
function sanitise(source) {
|
|
18167
|
+
return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR, "$1 $2");
|
|
18168
|
+
}
|
|
18169
|
+
var IDENTIFIERS = new Set(["identifier", "shorthand_property_identifier_pattern"]);
|
|
18170
|
+
function boundIdentifiers(node, text) {
|
|
18171
|
+
if (IDENTIFIERS.has(node.type))
|
|
18172
|
+
return [text(node)];
|
|
18173
|
+
const pattern = node.childForFieldName("pattern");
|
|
18174
|
+
if (pattern)
|
|
18175
|
+
return boundIdentifiers(pattern, text);
|
|
18176
|
+
const into = (child) => child ? boundIdentifiers(child, text) : [];
|
|
18177
|
+
switch (node.type) {
|
|
18178
|
+
case "object_pattern":
|
|
18179
|
+
case "array_pattern":
|
|
18180
|
+
return node.namedChildren.flatMap((child) => boundIdentifiers(child, text));
|
|
18181
|
+
case "assignment_pattern":
|
|
18182
|
+
case "object_assignment_pattern":
|
|
18183
|
+
return into(node.childForFieldName("left"));
|
|
18184
|
+
case "pair_pattern":
|
|
18185
|
+
return into(node.childForFieldName("value"));
|
|
18186
|
+
case "rest_pattern":
|
|
18187
|
+
return into(node.namedChildren[0] ?? null);
|
|
18188
|
+
default: {
|
|
18189
|
+
const first = node.namedChildren.find((c) => IDENTIFIERS.has(c.type));
|
|
18190
|
+
return first ? [text(first)] : [];
|
|
18191
|
+
}
|
|
18192
|
+
}
|
|
18193
|
+
}
|
|
18194
|
+
function paramsOf2(node, text) {
|
|
18195
|
+
const list = node.childForFieldName("parameters");
|
|
18196
|
+
return (list?.namedChildren ?? []).flatMap((param) => boundIdentifiers(param, text));
|
|
18197
|
+
}
|
|
18198
|
+
function anchorOf(node) {
|
|
18199
|
+
let target = node;
|
|
18200
|
+
while (target.parent?.type === "export_statement")
|
|
18201
|
+
target = target.parent;
|
|
18202
|
+
return target;
|
|
18203
|
+
}
|
|
18204
|
+
function leadingDoc(anchor, text) {
|
|
18205
|
+
const prev = anchor.previousSibling;
|
|
18206
|
+
if (prev?.type !== "comment")
|
|
18207
|
+
return "";
|
|
18208
|
+
const comment = text(prev);
|
|
18209
|
+
return comment.startsWith("/**") ? comment : "";
|
|
18210
|
+
}
|
|
18211
|
+
function isExported2(node) {
|
|
18212
|
+
let parent = node.parent;
|
|
18213
|
+
while (parent) {
|
|
18214
|
+
if (parent.type === "export_statement")
|
|
18215
|
+
return true;
|
|
18216
|
+
if (["program", "statement_block", "class_body"].includes(parent.type))
|
|
18217
|
+
break;
|
|
18218
|
+
parent = parent.parent;
|
|
18219
|
+
}
|
|
18220
|
+
return false;
|
|
18221
|
+
}
|
|
18222
|
+
function summaryOf2(doc) {
|
|
18223
|
+
for (const raw of pySplitlines(doc)) {
|
|
18224
|
+
const line = pyStrip(pyStrip(pyStrip(raw.replace(/\*+\/\s*$/, "")).replace(/^[/*]+/, "")).replace(/^\*+/, ""));
|
|
18225
|
+
if (line === "" || line.startsWith("@"))
|
|
18226
|
+
continue;
|
|
18227
|
+
return line;
|
|
18228
|
+
}
|
|
18229
|
+
return "";
|
|
18230
|
+
}
|
|
18231
|
+
function proseOfComment(doc) {
|
|
18232
|
+
const lines = pySplitlines(doc).map((l) => l.replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, ""));
|
|
18233
|
+
return proseOf(lines.join(`
|
|
18234
|
+
`));
|
|
18235
|
+
}
|
|
18236
|
+
function parsesAsTypeScript(path, text) {
|
|
18237
|
+
const tree = parserFor(grammarFor(path)).parse(sanitise(text.replace(/^\uFEFF/, "")));
|
|
18238
|
+
if (!tree)
|
|
18239
|
+
return false;
|
|
18240
|
+
try {
|
|
18241
|
+
return !tree.rootNode.hasError;
|
|
18242
|
+
} finally {
|
|
18243
|
+
tree.delete();
|
|
18244
|
+
}
|
|
18245
|
+
}
|
|
18246
|
+
async function analyseTypeScript(path, source) {
|
|
18247
|
+
const report = newReport(path, "typescript");
|
|
18248
|
+
const grammar = grammarFor(path);
|
|
18249
|
+
await loadGrammar(grammar);
|
|
18250
|
+
const tree = parserFor(grammar).parse(sanitise(source));
|
|
18251
|
+
if (!tree) {
|
|
18252
|
+
report.error = "parse error (the parser returned no tree)";
|
|
18253
|
+
return report;
|
|
18254
|
+
}
|
|
18255
|
+
try {
|
|
18256
|
+
if (tree.rootNode.hasError) {
|
|
18257
|
+
report.error = "parse error (tree-sitter reported ERROR nodes)";
|
|
18258
|
+
return report;
|
|
18259
|
+
}
|
|
18260
|
+
const text = (node) => source.slice(node.startIndex, node.endIndex);
|
|
18261
|
+
for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
|
|
18262
|
+
const kind = DECLARATIONS[node.type];
|
|
18263
|
+
const anchor = anchorOf(node);
|
|
18264
|
+
const doc = leadingDoc(anchor, text);
|
|
18265
|
+
const dialect = dialectOf(doc);
|
|
18266
|
+
const typeLike = kind === "class" || kind === "interface" || kind === "type";
|
|
18267
|
+
const line = node.startPosition.row + 1;
|
|
18268
|
+
const nameNode = node.childForFieldName("name");
|
|
18269
|
+
const sym = {
|
|
18270
|
+
file: path,
|
|
18271
|
+
line,
|
|
18272
|
+
anchorLine: anchor.startPosition.row + 1,
|
|
18273
|
+
anchorColumn: anchor.startPosition.column,
|
|
18274
|
+
name: nameNode ? text(nameNode) : "<anonymous>",
|
|
18275
|
+
kind,
|
|
18276
|
+
exported: isExported2(node),
|
|
18277
|
+
hasDoc: doc !== "",
|
|
18278
|
+
params: typeLike ? [] : paramsOf2(node, text),
|
|
18279
|
+
docParams: doc ? [...doc.matchAll(TSDOC_PARAM)].map((m) => m[1] ?? "") : [],
|
|
18280
|
+
summary: summaryOf2(doc),
|
|
18281
|
+
dialect,
|
|
18282
|
+
issues: []
|
|
18283
|
+
};
|
|
18284
|
+
const typed = /\.tsx?$/.test(path) && JSDOC_TYPED_PARAM.test(doc);
|
|
18285
|
+
applyRules(sym, doc, path, {
|
|
18286
|
+
noun: "doc comment",
|
|
18287
|
+
restatesType: typed ? "@param restates a type already in the TypeScript signature" : null,
|
|
18288
|
+
prose: proseOfComment(doc)
|
|
18289
|
+
});
|
|
18290
|
+
report.symbols.push(sym);
|
|
18291
|
+
if (dialect)
|
|
18292
|
+
report.dialects.add(dialect);
|
|
18293
|
+
}
|
|
18294
|
+
} finally {
|
|
18295
|
+
tree.delete();
|
|
18296
|
+
}
|
|
18297
|
+
report.symbols.sort((a, b) => a.line - b.line);
|
|
18298
|
+
return report;
|
|
18299
|
+
}
|
|
18300
|
+
|
|
18301
|
+
// src/docs/analyse.ts
|
|
18302
|
+
var decoder2 = new TextDecoder("utf-8", { fatal: true });
|
|
18303
|
+
function readFailure(error) {
|
|
18304
|
+
const code = error.code;
|
|
18305
|
+
if (code === "ENOENT")
|
|
18306
|
+
return "the file is tracked but missing from the work tree";
|
|
18307
|
+
if (error instanceof TypeError)
|
|
18308
|
+
return "the file is not valid UTF-8";
|
|
18309
|
+
return code ?? String(error);
|
|
18310
|
+
}
|
|
18311
|
+
function readSourceText2(path) {
|
|
18312
|
+
return decoder2.decode(readFileSync22(path)).replace(/\r\n?/g, `
|
|
18313
|
+
`);
|
|
18314
|
+
}
|
|
18315
|
+
async function analyseFile(root, rel) {
|
|
18316
|
+
const language = languageOf(rel);
|
|
18317
|
+
let source;
|
|
18318
|
+
try {
|
|
18319
|
+
source = readSourceText2(join24(root, rel));
|
|
18320
|
+
} catch (error) {
|
|
18321
|
+
const report = newReport(rel, language);
|
|
18322
|
+
report.error = `unreadable: ${readFailure(error)}`;
|
|
18323
|
+
return report;
|
|
18324
|
+
}
|
|
18325
|
+
return language === "python" ? analysePython(rel, source) : analyseTypeScript(rel, source);
|
|
18326
|
+
}
|
|
18327
|
+
async function analyseRepo(root) {
|
|
18328
|
+
const { files, provenance } = discover2(root);
|
|
18329
|
+
const reports = [];
|
|
18330
|
+
for (const rel of files)
|
|
18331
|
+
reports.push(await analyseFile(root, rel));
|
|
18332
|
+
return { reports, provenance };
|
|
18333
|
+
}
|
|
18334
|
+
|
|
18335
|
+
// src/docs/report.ts
|
|
18336
|
+
import { join as join25 } from "node:path";
|
|
18337
|
+
function percent3(part, whole) {
|
|
18338
|
+
if (whole === 0)
|
|
18339
|
+
return 100;
|
|
18340
|
+
const x = 100 * part / whole;
|
|
18341
|
+
const quarters = x * 4;
|
|
18342
|
+
if (Number.isInteger(quarters) && quarters % 2 !== 0) {
|
|
18343
|
+
const floor = Math.floor(x * 10);
|
|
18344
|
+
return (floor % 2 === 0 ? floor : floor + 1) / 10;
|
|
18345
|
+
}
|
|
18346
|
+
return Number(x.toFixed(1));
|
|
18347
|
+
}
|
|
18348
|
+
function summarise(reports) {
|
|
18349
|
+
const parsed = reports.filter(isParsed);
|
|
18350
|
+
const symbols = parsed.flatMap((r) => r.symbols);
|
|
18351
|
+
const exported = symbols.filter((s) => s.exported);
|
|
18352
|
+
const documented = exported.filter((s) => s.hasDoc);
|
|
18353
|
+
const issues = symbols.flatMap((s) => s.issues);
|
|
18354
|
+
return {
|
|
18355
|
+
filesScanned: reports.length,
|
|
18356
|
+
filesParsed: parsed.length,
|
|
18357
|
+
filesUnparsed: reports.length - parsed.length,
|
|
18358
|
+
symbols: symbols.length,
|
|
18359
|
+
exportedSymbols: exported.length,
|
|
18360
|
+
exportedDocumented: documented.length,
|
|
18361
|
+
exportedDocumentedPct: percent3(documented.length, exported.length),
|
|
18362
|
+
mustIssues: issues.filter((i2) => i2.tier === "MUST").length,
|
|
18363
|
+
shouldIssues: issues.filter((i2) => i2.tier === "SHOULD").length,
|
|
18364
|
+
filesMixedDialect: parsed.filter((r) => r.dialects.size > 1).length
|
|
18365
|
+
};
|
|
18366
|
+
}
|
|
18367
|
+
var TABLE_LIMIT = 200;
|
|
18368
|
+
var pctText = (pct) => pct.toFixed(1);
|
|
18369
|
+
function renderMarkdown(reports, stats, provenance, rootName) {
|
|
18370
|
+
const lines = [
|
|
18371
|
+
"# Code documentation coverage",
|
|
18372
|
+
"",
|
|
18373
|
+
`**Repo:** \`${rootName}\``,
|
|
18374
|
+
"",
|
|
18375
|
+
"Generated by `repo-tools docs scan`. The rules are in the README of `repo-tools`.",
|
|
18376
|
+
"",
|
|
18377
|
+
"## Summary",
|
|
18378
|
+
"",
|
|
18379
|
+
"| Metric | Value |",
|
|
18380
|
+
"|---|---:|",
|
|
18381
|
+
`| Files scanned | ${stats.filesScanned} |`,
|
|
18382
|
+
`| Files parsed | ${stats.filesParsed} |`,
|
|
18383
|
+
`| **Files UNPARSED** (unknown, not clean) | **${stats.filesUnparsed}** |`,
|
|
18384
|
+
`| Symbols found | ${stats.symbols} |`,
|
|
18385
|
+
`| Exported symbols | ${stats.exportedSymbols} |`,
|
|
18386
|
+
`| Exported documented | ${stats.exportedDocumented} |`,
|
|
18387
|
+
`| Exported documented % | ${pctText(stats.exportedDocumentedPct)} |`,
|
|
18388
|
+
`| MUST issues (gated) | ${stats.mustIssues} |`,
|
|
18389
|
+
`| SHOULD issues (advisory) | ${stats.shouldIssues} |`,
|
|
18390
|
+
`| Files with mixed dialect | ${stats.filesMixedDialect} |`,
|
|
18391
|
+
"",
|
|
18392
|
+
"> The percentage is over **exported** symbols only. Private helpers are not",
|
|
18393
|
+
"> gated -- the public surface is a contract, internals are judgement.",
|
|
18394
|
+
""
|
|
18395
|
+
];
|
|
18396
|
+
const mustRows = reports.filter(isParsed).flatMap((r) => r.symbols.flatMap((s) => s.issues.map((i2) => ({ s, i: i2 })))).filter(({ i: i2 }) => i2.tier === "MUST");
|
|
18397
|
+
if (mustRows.length > 0) {
|
|
18398
|
+
lines.push("## MUST issues", "", "| File | Line | Symbol | Rule | Detail |", "|---|---:|---|---|---|");
|
|
18399
|
+
for (const { s, i: i2 } of mustRows.slice(0, TABLE_LIMIT)) {
|
|
18400
|
+
lines.push(`| \`${s.file}\` | ${s.line} | \`${s.name}\` | ${i2.rule} | ${i2.detail} |`);
|
|
18401
|
+
}
|
|
18402
|
+
if (mustRows.length > TABLE_LIMIT) {
|
|
18403
|
+
lines.push(`| … | | | | **${mustRows.length - TABLE_LIMIT} more not shown -- this table is TRUNCATED** |`);
|
|
18404
|
+
}
|
|
18405
|
+
lines.push("");
|
|
18406
|
+
}
|
|
18407
|
+
const unparsed = reports.filter((r) => !isParsed(r));
|
|
18408
|
+
if (unparsed.length > 0) {
|
|
18409
|
+
lines.push("## Unparsed files", "", "These were NOT measured. They are unknown, not clean.", "");
|
|
18410
|
+
for (const r of unparsed)
|
|
18411
|
+
lines.push(`- \`${r.path}\` -- ${r.error}`);
|
|
18412
|
+
lines.push("");
|
|
18413
|
+
}
|
|
18414
|
+
lines.push("## Verification", "", `- files_scanned: ${stats.filesScanned}`, `- files_parsed: ${stats.filesParsed}`, `- files_unparsed: ${stats.filesUnparsed}`, `- exported_symbols: ${stats.exportedSymbols}`, `- exported_documented: ${stats.exportedDocumented}`, `- must_issues: ${stats.mustIssues}`, `- discovery: ${provenance}`, "", "Run `repo-tools docs check .` to gate the repository. The command measures the code again and", "exits 1 on a MUST issue or on an unparsed file. It does not read this file.", "");
|
|
18415
|
+
return lines.join(`
|
|
18416
|
+
`);
|
|
18417
|
+
}
|
|
18418
|
+
function statsJson(stats) {
|
|
18419
|
+
return {
|
|
18420
|
+
files_scanned: stats.filesScanned,
|
|
18421
|
+
files_parsed: stats.filesParsed,
|
|
18422
|
+
files_unparsed: stats.filesUnparsed,
|
|
18423
|
+
symbols: stats.symbols,
|
|
18424
|
+
exported_symbols: stats.exportedSymbols,
|
|
18425
|
+
exported_documented: stats.exportedDocumented,
|
|
18426
|
+
exported_documented_pct: stats.exportedDocumentedPct,
|
|
18427
|
+
must_issues: stats.mustIssues,
|
|
18428
|
+
should_issues: stats.shouldIssues,
|
|
18429
|
+
files_mixed_dialect: stats.filesMixedDialect
|
|
18430
|
+
};
|
|
18431
|
+
}
|
|
18432
|
+
function symbolJson(s) {
|
|
18433
|
+
return {
|
|
18434
|
+
file: s.file,
|
|
18435
|
+
line: s.line,
|
|
18436
|
+
name: s.name,
|
|
18437
|
+
kind: s.kind,
|
|
18438
|
+
exported: s.exported,
|
|
18439
|
+
has_doc: s.hasDoc,
|
|
18440
|
+
params: s.params,
|
|
18441
|
+
doc_params: s.docParams,
|
|
18442
|
+
summary: s.summary,
|
|
18443
|
+
dialect: s.dialect,
|
|
18444
|
+
issues: s.issues
|
|
18445
|
+
};
|
|
18446
|
+
}
|
|
18447
|
+
function writeOutputs(outDir, reports, stats, provenance, rootName) {
|
|
18448
|
+
const markdown = join25(outDir, "COVERAGE.md");
|
|
18449
|
+
const json = join25(outDir, "coverage.json");
|
|
18450
|
+
writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
|
|
18451
|
+
const payload = {
|
|
18452
|
+
summary: statsJson(stats),
|
|
18453
|
+
provenance,
|
|
18454
|
+
files: reports.map((r) => ({
|
|
18455
|
+
path: r.path,
|
|
18456
|
+
language: r.language,
|
|
18457
|
+
error: r.error,
|
|
18458
|
+
dialects: [...r.dialects].sort(),
|
|
18459
|
+
symbols: r.symbols.map(symbolJson)
|
|
18460
|
+
}))
|
|
18461
|
+
};
|
|
18462
|
+
writeReport(json, toJson(payload));
|
|
18463
|
+
return { markdown, json };
|
|
18464
|
+
}
|
|
18465
|
+
|
|
18466
|
+
// src/docs/stub.ts
|
|
18467
|
+
function splitLines(text) {
|
|
18468
|
+
return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
18469
|
+
}
|
|
18470
|
+
var indentOf = (line) => line.slice(0, line.length - pyLstrip(line).length);
|
|
18471
|
+
var OPENERS = "([{";
|
|
18472
|
+
var CLOSERS = ")]}";
|
|
18473
|
+
function codeOf(line, open) {
|
|
18474
|
+
let code = "";
|
|
18475
|
+
let string = open;
|
|
18476
|
+
let i2 = 0;
|
|
18477
|
+
while (i2 < line.length) {
|
|
18478
|
+
const ch = line.charAt(i2);
|
|
18479
|
+
if (string !== "") {
|
|
18480
|
+
if (ch === "\\")
|
|
18481
|
+
i2 += 2;
|
|
18482
|
+
else if (line.startsWith(string, i2)) {
|
|
18483
|
+
i2 += string.length;
|
|
18484
|
+
string = "";
|
|
18485
|
+
} else
|
|
18486
|
+
i2 += 1;
|
|
18487
|
+
} else if (ch === "#") {
|
|
18488
|
+
break;
|
|
18489
|
+
} else if (ch === '"' || ch === "'") {
|
|
18490
|
+
string = line.startsWith(ch.repeat(3), i2) ? ch.repeat(3) : ch;
|
|
18491
|
+
i2 += string.length;
|
|
18492
|
+
} else {
|
|
18493
|
+
code += ch;
|
|
18494
|
+
i2 += 1;
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18497
|
+
return { code, open: string.length === 3 ? string : "" };
|
|
18498
|
+
}
|
|
18499
|
+
function headerEnd(lines, start2) {
|
|
18500
|
+
let depth = 0;
|
|
18501
|
+
let open = "";
|
|
18502
|
+
for (let i2 = start2;i2 < Math.min(lines.length, start2 + 60); i2++) {
|
|
18503
|
+
const line = codeOf(lines[i2] ?? "", open);
|
|
18504
|
+
open = line.open;
|
|
18505
|
+
for (const ch of line.code) {
|
|
18506
|
+
if (OPENERS.includes(ch))
|
|
18507
|
+
depth += 1;
|
|
18508
|
+
else if (CLOSERS.includes(ch))
|
|
18509
|
+
depth -= 1;
|
|
18510
|
+
}
|
|
18511
|
+
if (depth <= 0 && line.code.trimEnd().endsWith(":"))
|
|
18512
|
+
return i2;
|
|
18513
|
+
}
|
|
18514
|
+
return start2;
|
|
18515
|
+
}
|
|
18516
|
+
function planPython(sym, lines) {
|
|
18517
|
+
if (sym.hasDoc)
|
|
18518
|
+
return null;
|
|
18519
|
+
const start2 = sym.line - 1;
|
|
18520
|
+
if (start2 >= lines.length)
|
|
18521
|
+
return null;
|
|
18522
|
+
const end = headerEnd(lines, start2);
|
|
18523
|
+
const body2 = `${indentOf(lines[end] ?? "")} `;
|
|
18524
|
+
const out2 = [`${body2}"""${STUB_MARKER} state what this does, in one active-voice sentence.`];
|
|
18525
|
+
if (sym.params.length > 0 || sym.kind === "function")
|
|
18526
|
+
out2.push("");
|
|
18527
|
+
if (sym.params.length > 0) {
|
|
18528
|
+
out2.push(`${body2}Args:`);
|
|
18529
|
+
for (const p of sym.params) {
|
|
18530
|
+
out2.push(`${body2} ${p}: ${STUB_MARKER} state what it is. Do not repeat the type.`);
|
|
18531
|
+
}
|
|
18532
|
+
out2.push("");
|
|
18533
|
+
}
|
|
18534
|
+
if (sym.kind === "function") {
|
|
18535
|
+
out2.push(`${body2}Returns:`, `${body2} ${STUB_MARKER} state what the caller gets back.`);
|
|
18536
|
+
}
|
|
18537
|
+
out2.push(`${body2}"""`);
|
|
18538
|
+
return { lineIndex: end + 1, text: `${out2.join(`
|
|
18539
|
+
`)}
|
|
18540
|
+
`, symbol: sym.name };
|
|
18541
|
+
}
|
|
18542
|
+
function startsItsLine(sym, lines) {
|
|
18543
|
+
const line = lines[sym.anchorLine - 1] ?? "";
|
|
18544
|
+
return line.slice(0, sym.anchorColumn).trim() === "";
|
|
18545
|
+
}
|
|
18546
|
+
function planTypeScript(sym, lines) {
|
|
18547
|
+
if (sym.hasDoc)
|
|
18548
|
+
return null;
|
|
18549
|
+
const index = sym.anchorLine - 1;
|
|
18550
|
+
if (index >= lines.length)
|
|
18551
|
+
return null;
|
|
18552
|
+
const indent = indentOf(lines[index] ?? "");
|
|
18553
|
+
const out2 = [
|
|
18554
|
+
`${indent}/**`,
|
|
18555
|
+
`${indent} * ${STUB_MARKER} state what this does, in one active-voice sentence.`
|
|
18556
|
+
];
|
|
18557
|
+
if (sym.params.length > 0) {
|
|
18558
|
+
out2.push(`${indent} *`);
|
|
18559
|
+
for (const p of sym.params) {
|
|
18560
|
+
out2.push(`${indent} * @param ${p} - ${STUB_MARKER} state what it is. Do not repeat the type.`);
|
|
18561
|
+
}
|
|
18562
|
+
}
|
|
18563
|
+
if (sym.kind === "function" || sym.kind === "method") {
|
|
18564
|
+
out2.push(`${indent} * @returns ${STUB_MARKER} state what the caller gets back.`);
|
|
18565
|
+
}
|
|
18566
|
+
out2.push(`${indent} */`);
|
|
18567
|
+
return { lineIndex: index, text: `${out2.join(`
|
|
18568
|
+
`)}
|
|
18569
|
+
`, symbol: sym.name };
|
|
18570
|
+
}
|
|
18571
|
+
function applyInsertions(original, insertions, verify) {
|
|
18572
|
+
if (insertions.length === 0)
|
|
18573
|
+
return { ok: false, text: original, message: "no insertions" };
|
|
18574
|
+
const eol = original.includes(`\r
|
|
18575
|
+
`) ? `\r
|
|
18576
|
+
` : `
|
|
18577
|
+
`;
|
|
18578
|
+
const lines = splitLines(original);
|
|
18579
|
+
for (const ins of [...insertions].sort((a, b) => b.lineIndex - a.lineIndex)) {
|
|
18580
|
+
const index = Math.max(0, Math.min(ins.lineIndex, lines.length));
|
|
18581
|
+
lines.splice(index, 0, eol === `
|
|
18582
|
+
` ? ins.text : ins.text.replace(/\n/g, eol));
|
|
18583
|
+
}
|
|
18584
|
+
const updated = lines.join("");
|
|
18585
|
+
if (!verify(updated)) {
|
|
18586
|
+
return {
|
|
18587
|
+
ok: false,
|
|
18588
|
+
text: original,
|
|
18589
|
+
message: "REVERTED: the file would no longer parse after insertion"
|
|
18590
|
+
};
|
|
18591
|
+
}
|
|
18592
|
+
return { ok: true, text: updated, message: `inserted ${insertions.length} stub(s)` };
|
|
18593
|
+
}
|
|
18594
|
+
|
|
18595
|
+
// src/docs/index.ts
|
|
18596
|
+
var DOCS_HELP = `Usage: repo-tools docs scan [root] [--out=<dir>]
|
|
18597
|
+
repo-tools docs stub [root] [--apply] [--path=<path>]
|
|
18598
|
+
repo-tools docs check [root] [--paths <path>... | --paths-from=<file>]
|
|
18599
|
+
|
|
18600
|
+
Measure, stub and gate the documentation comments of the TypeScript, JavaScript
|
|
18601
|
+
and Python files of a repository. The files are the files that git tracks. A
|
|
18602
|
+
file in a skipped folder (node_modules, dist, build, bundle, out, vendor) or
|
|
18603
|
+
with a generated name (.d.ts, .min.js, _pb2.py) is not read.
|
|
18604
|
+
|
|
18605
|
+
Actions:
|
|
18606
|
+
scan Measure the coverage. Write COVERAGE.md and coverage.json.
|
|
18607
|
+
stub Plan a skeleton comment for each exported symbol that has none. The
|
|
18608
|
+
default is a dry run. Every stub holds the marker TODO:, which check
|
|
18609
|
+
fails on.
|
|
18610
|
+
check The gate. Exit 1 on a MUST issue or on a file that does not parse.
|
|
18611
|
+
|
|
18612
|
+
Options:
|
|
18613
|
+
--root=<path> The repository (default: the current folder). An argument
|
|
18614
|
+
that is not a flag also sets the root.
|
|
18615
|
+
--out=<dir> scan: the output folder (default: docs/code-docs below
|
|
18616
|
+
the root). A relative path is relative to the current
|
|
18617
|
+
folder.
|
|
18618
|
+
--apply stub: write the stubs. A file that would not parse after
|
|
18619
|
+
the change is not changed.
|
|
18620
|
+
--path=<path> stub: only this file, or the files below this folder. The
|
|
18621
|
+
root itself ('.') selects every file.
|
|
18622
|
+
--paths <path>... check: gate only these files, for example the files of a
|
|
18623
|
+
pull request. Put the root before this flag. The flag
|
|
18624
|
+
takes every argument up to the next flag. No path means
|
|
18625
|
+
that the gate checks nothing, and it passes.
|
|
18626
|
+
--paths-from=<file> check: read the paths from a file, one path on each line,
|
|
18627
|
+
as 'git diff --name-only' writes them. The quoted form
|
|
18628
|
+
that git writes for a non-ASCII name is decoded.
|
|
18629
|
+
--help, -h Show this help.
|
|
18630
|
+
|
|
18631
|
+
A flag with a value takes '--name=value' or '--name value'.
|
|
18632
|
+
|
|
18633
|
+
The MUST rules decide the exit code:
|
|
18634
|
+
M1 An exported symbol has a doc comment. A test file is exempt.
|
|
18635
|
+
M2 The doc comment has a summary line.
|
|
18636
|
+
M3 The doc comment names no parameter that the signature lacks.
|
|
18637
|
+
M4 The doc comment holds no TODO: marker from stub.
|
|
18638
|
+
M5 A file uses one doc dialect only (google, numpy, rest, tsdoc or jsdoc).
|
|
18639
|
+
The SHOULD rules are reported and never gate: S2 (the summary ends with a full
|
|
18640
|
+
stop), S5 (the comment repeats a type that the signature holds), and S6 (the
|
|
18641
|
+
prose follows Simplified Technical English).
|
|
18642
|
+
|
|
18643
|
+
${CONFIG_NAME} at the root excludes paths from every action. Each entry needs a
|
|
18644
|
+
reason, and the output names each exclusion:
|
|
18645
|
+
{ "exclude": [ { "path": "assembly/", "reason": "AssemblyScript" } ] }
|
|
18646
|
+
|
|
18647
|
+
Exit codes: 0 when the action succeeded and the gate passed. 1 when the gate
|
|
18648
|
+
failed, or when the root holds no source file to measure. 2 on a usage error,
|
|
18649
|
+
when the root is not a folder, or when ${CONFIG_NAME} is malformed.
|
|
18650
|
+
`;
|
|
18651
|
+
var VALUE_FLAGS2 = {
|
|
18652
|
+
"--root": "all",
|
|
18653
|
+
"--out": "scan",
|
|
18654
|
+
"--path": "stub",
|
|
18655
|
+
"--paths-from": "check"
|
|
18656
|
+
};
|
|
18657
|
+
function parseDocsArgs(argv) {
|
|
18658
|
+
const action = argv[0];
|
|
18659
|
+
if (action !== "scan" && action !== "stub" && action !== "check") {
|
|
18660
|
+
throw new Error(action === undefined ? "an action is required: scan, stub or check (see repo-tools docs --help)" : `unknown action '${action}': use scan, stub or check`);
|
|
18661
|
+
}
|
|
18662
|
+
const options = {
|
|
18663
|
+
action,
|
|
18664
|
+
root: ".",
|
|
18665
|
+
out: undefined,
|
|
18666
|
+
apply: false,
|
|
18667
|
+
path: undefined,
|
|
18668
|
+
paths: undefined,
|
|
18669
|
+
pathsFrom: undefined
|
|
18670
|
+
};
|
|
18671
|
+
let rootSet = false;
|
|
18672
|
+
const setRoot = (value) => {
|
|
18673
|
+
if (rootSet)
|
|
18674
|
+
throw new Error("the root is set twice (use --root=<path> or one positional path)");
|
|
18675
|
+
rootSet = true;
|
|
18676
|
+
options.root = value;
|
|
18677
|
+
};
|
|
18678
|
+
const forbid = (flag, owner) => {
|
|
18679
|
+
if (owner !== "all" && owner !== action)
|
|
18680
|
+
throw new Error(`flag ${flag} applies to '${owner}' only`);
|
|
18681
|
+
};
|
|
18682
|
+
for (let i2 = 1;i2 < argv.length; i2++) {
|
|
18683
|
+
const arg = argv[i2] ?? "";
|
|
18684
|
+
if (!arg.startsWith("-")) {
|
|
18685
|
+
setRoot(arg);
|
|
18686
|
+
continue;
|
|
18687
|
+
}
|
|
18688
|
+
const eq = arg.indexOf("=");
|
|
18689
|
+
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
18690
|
+
if (name2 === "--apply") {
|
|
18691
|
+
forbid(name2, "stub");
|
|
18692
|
+
if (eq !== -1)
|
|
18693
|
+
throw new Error("flag --apply takes no value");
|
|
18694
|
+
options.apply = true;
|
|
18695
|
+
} else if (name2 === "--paths") {
|
|
18696
|
+
forbid(name2, "check");
|
|
18697
|
+
options.paths ??= [];
|
|
18698
|
+
if (eq !== -1) {
|
|
18699
|
+
options.paths.push(arg.slice(eq + 1));
|
|
18700
|
+
} else {
|
|
18701
|
+
while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
|
|
18702
|
+
options.paths.push(argv[++i2] ?? "");
|
|
18703
|
+
}
|
|
18704
|
+
} else if (Object.hasOwn(VALUE_FLAGS2, name2)) {
|
|
18705
|
+
forbid(name2, VALUE_FLAGS2[name2]);
|
|
18706
|
+
let value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
18707
|
+
if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
|
|
18708
|
+
value = argv[++i2];
|
|
18709
|
+
if (value === undefined || value === "") {
|
|
18710
|
+
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
18711
|
+
}
|
|
18712
|
+
if (name2 === "--root")
|
|
18713
|
+
setRoot(value);
|
|
18714
|
+
else if (name2 === "--out")
|
|
18715
|
+
options.out = value;
|
|
18716
|
+
else if (name2 === "--path")
|
|
18717
|
+
options.path = value;
|
|
18718
|
+
else
|
|
18719
|
+
options.pathsFrom = value;
|
|
18720
|
+
} else {
|
|
18721
|
+
throw new Error(`unknown flag '${name2}' (see repo-tools docs --help)`);
|
|
18722
|
+
}
|
|
18723
|
+
}
|
|
18724
|
+
if (options.paths !== undefined && options.pathsFrom !== undefined) {
|
|
18725
|
+
throw new Error("use --paths or --paths-from, not both");
|
|
18726
|
+
}
|
|
18727
|
+
return options;
|
|
18728
|
+
}
|
|
18729
|
+
function relativeToRoot(root, path) {
|
|
18730
|
+
let q = toPosix(path);
|
|
18731
|
+
if (isAbsolute4(path)) {
|
|
18732
|
+
const rel = toPosix(relative5(root, path));
|
|
18733
|
+
if (rel !== ".." && !rel.startsWith("../") && !isAbsolute4(rel))
|
|
18734
|
+
q = rel;
|
|
18735
|
+
}
|
|
18736
|
+
return q.replace(/^(\.\/)+/, "");
|
|
18737
|
+
}
|
|
18738
|
+
function leavesRoot(root, file) {
|
|
18739
|
+
try {
|
|
18740
|
+
if (lstatSync5(file).isSymbolicLink())
|
|
18741
|
+
return true;
|
|
18742
|
+
const rel = relative5(realpathSync3.native(root), realpathSync3.native(file));
|
|
18743
|
+
return rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel);
|
|
18744
|
+
} catch {
|
|
18745
|
+
return true;
|
|
18746
|
+
}
|
|
18747
|
+
}
|
|
18748
|
+
var GIT_ESCAPES = {
|
|
18749
|
+
a: 7,
|
|
18750
|
+
b: 8,
|
|
18751
|
+
t: 9,
|
|
18752
|
+
n: 10,
|
|
18753
|
+
v: 11,
|
|
18754
|
+
f: 12,
|
|
18755
|
+
r: 13,
|
|
18756
|
+
'"': 34,
|
|
18757
|
+
"\\": 92
|
|
18758
|
+
};
|
|
18759
|
+
function unquoteGitPath(line) {
|
|
18760
|
+
if (line.length < 2 || !line.startsWith('"') || !line.endsWith('"'))
|
|
18761
|
+
return line;
|
|
18762
|
+
const chars = Array.from(line.slice(1, -1));
|
|
18763
|
+
const encoder = new TextEncoder;
|
|
18764
|
+
const bytes = [];
|
|
18765
|
+
for (let i2 = 0;i2 < chars.length; i2++) {
|
|
18766
|
+
const ch = chars[i2] ?? "";
|
|
18767
|
+
const octal = chars.slice(i2 + 1, i2 + 4).join("");
|
|
18768
|
+
const single = GIT_ESCAPES[chars[i2 + 1] ?? ""];
|
|
18769
|
+
if (ch !== "\\") {
|
|
18770
|
+
bytes.push(...encoder.encode(ch));
|
|
18771
|
+
} else if (/^[0-7]{3}$/.test(octal)) {
|
|
18772
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
18773
|
+
i2 += 3;
|
|
18774
|
+
} else if (single !== undefined) {
|
|
18775
|
+
bytes.push(single);
|
|
18776
|
+
i2 += 1;
|
|
18777
|
+
} else {
|
|
18778
|
+
bytes.push(...encoder.encode(ch));
|
|
18779
|
+
}
|
|
18780
|
+
}
|
|
18781
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
18782
|
+
}
|
|
18783
|
+
function shown2(root, path) {
|
|
18784
|
+
const rel = relative5(root, path);
|
|
18785
|
+
return rel.startsWith("..") || isAbsolute4(rel) ? path : `<root>/${toPosix(rel)}`;
|
|
18786
|
+
}
|
|
18787
|
+
|
|
18788
|
+
class NothingToMeasure extends Error {
|
|
18789
|
+
constructor(provenance) {
|
|
18790
|
+
super("no TypeScript, JavaScript or Python source file was found below <root> " + `(${provenance}) -- nothing to measure. This is a failure, not a pass: check the path, ` + "or add the files to git.");
|
|
18791
|
+
this.name = "NothingToMeasure";
|
|
18792
|
+
}
|
|
18793
|
+
}
|
|
18794
|
+
async function runScan(root, options, io) {
|
|
18795
|
+
const { reports, provenance } = await analyseRepo(root);
|
|
18796
|
+
if (reports.length === 0)
|
|
18797
|
+
throw new NothingToMeasure(provenance);
|
|
18798
|
+
const stats = summarise(reports);
|
|
18799
|
+
const outDir = resolve8(options.out ?? join26(root, "docs", "code-docs"));
|
|
18800
|
+
const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
|
|
18801
|
+
io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
|
|
18802
|
+
`);
|
|
18803
|
+
io.stdout(` exported symbols : ${stats.exportedSymbols}
|
|
18804
|
+
`);
|
|
18805
|
+
io.stdout(` documented : ${stats.exportedDocumented} (${pctText(stats.exportedDocumentedPct)}%)
|
|
18806
|
+
`);
|
|
18807
|
+
io.stdout(` MUST issues : ${stats.mustIssues}
|
|
18808
|
+
`);
|
|
18809
|
+
io.stdout(` SHOULD issues : ${stats.shouldIssues}
|
|
18810
|
+
`);
|
|
18811
|
+
if (stats.filesUnparsed > 0) {
|
|
18812
|
+
io.stdout(` UNPARSED (unknown) : ${stats.filesUnparsed} <- not counted as clean
|
|
18813
|
+
`);
|
|
18814
|
+
}
|
|
18815
|
+
io.stdout(`wrote ${shown2(root, written.markdown)}
|
|
18816
|
+
`);
|
|
18817
|
+
io.stdout(`wrote ${shown2(root, written.json)}
|
|
18818
|
+
`);
|
|
18819
|
+
return 0;
|
|
18820
|
+
}
|
|
18821
|
+
function inScope(report, scope) {
|
|
18822
|
+
const prefix = `${scope.replace(/\/+$/, "")}/`;
|
|
18823
|
+
return report.path.startsWith(prefix) || report.path === scope;
|
|
18824
|
+
}
|
|
18825
|
+
async function runStub(root, options, io) {
|
|
18826
|
+
const analysis = await analyseRepo(root);
|
|
18827
|
+
if (analysis.reports.length === 0)
|
|
18828
|
+
throw new NothingToMeasure(analysis.provenance);
|
|
18829
|
+
const scope = options.path === undefined ? undefined : relativeToRoot(root, options.path).replace(/\/+$/, "");
|
|
18830
|
+
const reports = scope === undefined || scope === "" || scope === "." ? analysis.reports : analysis.reports.filter((r) => inScope(r, scope));
|
|
18831
|
+
let planned = 0;
|
|
18832
|
+
let changed = 0;
|
|
18833
|
+
for (const report of reports) {
|
|
18834
|
+
if (!isParsed(report))
|
|
18835
|
+
continue;
|
|
18836
|
+
const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
|
|
18837
|
+
if (targets.length === 0)
|
|
18838
|
+
continue;
|
|
18839
|
+
const file = join26(root, report.path);
|
|
18840
|
+
if (leavesRoot(root, file)) {
|
|
18841
|
+
io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
|
|
18842
|
+
`);
|
|
18843
|
+
continue;
|
|
18844
|
+
}
|
|
18845
|
+
const raw = readFileSync23(file, "utf8");
|
|
18846
|
+
if (/\r(?!\n)/.test(raw)) {
|
|
18847
|
+
io.stdout(` SKIPPED ${report.path}: the file holds a carriage return with no line feed
|
|
18848
|
+
`);
|
|
18849
|
+
continue;
|
|
18850
|
+
}
|
|
18851
|
+
const lines = splitLines(raw);
|
|
18852
|
+
const python = report.language === "python";
|
|
18853
|
+
const insertions = [];
|
|
18854
|
+
for (const symbol of targets) {
|
|
18855
|
+
if (!python && !startsItsLine(symbol, lines)) {
|
|
18856
|
+
io.stdout(` cannot place a stub for ${report.path}:${symbol.anchorLine} ${symbol.name}: ` + `another token precedes the declaration on its line
|
|
18857
|
+
`);
|
|
18858
|
+
continue;
|
|
18859
|
+
}
|
|
18860
|
+
const insertion = (python ? planPython : planTypeScript)(symbol, lines);
|
|
18861
|
+
if (insertion !== null)
|
|
18862
|
+
insertions.push(insertion);
|
|
18863
|
+
}
|
|
18864
|
+
planned += insertions.length;
|
|
18865
|
+
if (!options.apply) {
|
|
18866
|
+
for (const ins of insertions) {
|
|
18867
|
+
io.stdout(` would document ${report.path}:${ins.lineIndex + 1} ${ins.symbol}
|
|
18868
|
+
`);
|
|
18869
|
+
}
|
|
18870
|
+
continue;
|
|
18871
|
+
}
|
|
18872
|
+
const verify = report.language === "python" ? parsesAsPython : (text) => parsesAsTypeScript(report.path, text);
|
|
18873
|
+
const result = applyInsertions(raw, insertions, verify);
|
|
18874
|
+
if (result.ok)
|
|
18875
|
+
writeFileSync5(file, result.text);
|
|
18876
|
+
io.stdout(` ${result.ok ? "wrote" : "SKIPPED"} ${report.path}: ${result.message}
|
|
18877
|
+
`);
|
|
18878
|
+
if (result.ok)
|
|
18879
|
+
changed += 1;
|
|
18880
|
+
}
|
|
18881
|
+
const mode = options.apply ? "APPLIED" : "DRY RUN (pass --apply to write)";
|
|
18882
|
+
io.stdout(`${mode}: ${planned} stub(s) planned across ${reports.length} file(s); ${changed} file(s) modified
|
|
18883
|
+
`);
|
|
18884
|
+
if (planned > 0 && !options.apply) {
|
|
18885
|
+
io.stdout("Every stub carries a TODO: marker, which `check` FAILS on -- fill them in.\n");
|
|
18886
|
+
}
|
|
18887
|
+
return 0;
|
|
18888
|
+
}
|
|
18889
|
+
function requestedPaths(root, options) {
|
|
18890
|
+
let raw = options.paths;
|
|
18891
|
+
if (options.pathsFrom !== undefined) {
|
|
18892
|
+
let text;
|
|
18893
|
+
try {
|
|
18894
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync23(options.pathsFrom));
|
|
18895
|
+
} catch {
|
|
18896
|
+
throw new Error(`cannot read the paths file ${basename9(options.pathsFrom)} as UTF-8 text`);
|
|
18897
|
+
}
|
|
18898
|
+
raw = text.split(/\r\n|\r|\n/).map((l) => l.trim()).filter((l) => l !== "").map(unquoteGitPath);
|
|
18899
|
+
}
|
|
18900
|
+
return raw?.map((p) => relativeToRoot(root, p));
|
|
18901
|
+
}
|
|
18902
|
+
var pyList = (items) => `[${items.sort().map((s) => `'${s}'`).join(", ")}]`;
|
|
18903
|
+
async function runCheck(root, options, io) {
|
|
18904
|
+
const scope = requestedPaths(root, options);
|
|
18905
|
+
const { reports: all, provenance } = await analyseRepo(root);
|
|
18906
|
+
if (all.length === 0 && scope === undefined)
|
|
18907
|
+
throw new NothingToMeasure(provenance);
|
|
18908
|
+
const stats = summarise(all);
|
|
18909
|
+
const wanted = scope === undefined ? undefined : new Set(scope);
|
|
18910
|
+
const reports = wanted === undefined ? all : all.filter((r) => wanted.has(r.path));
|
|
18911
|
+
const failures = [];
|
|
18912
|
+
for (const report of reports) {
|
|
18913
|
+
if (!isParsed(report)) {
|
|
18914
|
+
failures.push(`${report.path}: UNPARSED -- ${report.error}`);
|
|
18915
|
+
continue;
|
|
18916
|
+
}
|
|
18917
|
+
if (report.dialects.size > 1) {
|
|
18918
|
+
failures.push(`${report.path}: M5 mixed docstring dialects ${pyList([...report.dialects])}`);
|
|
18919
|
+
}
|
|
18920
|
+
for (const sym of report.symbols) {
|
|
18921
|
+
for (const issue of sym.issues) {
|
|
18922
|
+
if (issue.tier === "MUST") {
|
|
18923
|
+
failures.push(`${sym.file}:${sym.line} ${sym.name}: ${issue.rule} ${issue.detail}`);
|
|
18924
|
+
}
|
|
18925
|
+
}
|
|
18926
|
+
}
|
|
18927
|
+
}
|
|
18928
|
+
if (wanted === undefined) {
|
|
18929
|
+
io.stdout(`checked ${stats.filesScanned} file(s) via ${provenance}
|
|
18930
|
+
`);
|
|
18931
|
+
} else {
|
|
18932
|
+
io.stdout(`checked ${reports.length} changed source file(s) of ${stats.filesScanned} tracked (${provenance})
|
|
18933
|
+
`);
|
|
18934
|
+
const known = new Set(all.map((r) => r.path));
|
|
18935
|
+
const missed = [...wanted].filter((p) => languageOf(p) !== "" && !known.has(p));
|
|
18936
|
+
if (missed.length > 0) {
|
|
18937
|
+
io.stdout(`note: ${missed.length} requested source path(s) were not measured (not tracked, excluded, ` + `deleted, or in a skipped folder): ${missed.slice(0, 10).join(", ")}${missed.length > 10 ? ", ..." : ""}
|
|
18938
|
+
`);
|
|
18939
|
+
}
|
|
18940
|
+
}
|
|
18941
|
+
if (failures.length === 0) {
|
|
18942
|
+
if (wanted === undefined) {
|
|
18943
|
+
io.stdout(`PASS -- ${stats.exportedDocumented}/${stats.exportedSymbols} exported symbols documented, 0 MUST issues
|
|
18944
|
+
`);
|
|
18945
|
+
if (stats.shouldIssues > 0) {
|
|
18946
|
+
io.stdout(` (${stats.shouldIssues} SHOULD issue(s) reported, not gated)
|
|
18947
|
+
`);
|
|
18948
|
+
}
|
|
18949
|
+
} else {
|
|
18950
|
+
io.stdout(`PASS -- 0 MUST issue(s) in the changed files
|
|
18951
|
+
`);
|
|
18952
|
+
}
|
|
18953
|
+
return 0;
|
|
18954
|
+
}
|
|
18955
|
+
io.stdout(`FAIL -- ${failures.length} MUST issue(s):
|
|
18956
|
+
`);
|
|
18957
|
+
for (const line of failures.slice(0, 100))
|
|
18958
|
+
io.stdout(` ${line}
|
|
18959
|
+
`);
|
|
18960
|
+
if (failures.length > 100)
|
|
18961
|
+
io.stdout(` ... and ${failures.length - 100} more (TRUNCATED)
|
|
18962
|
+
`);
|
|
18963
|
+
return 1;
|
|
18964
|
+
}
|
|
18965
|
+
async function run5(argv, io) {
|
|
18966
|
+
if (wantsHelp(argv)) {
|
|
18967
|
+
io.stdout(DOCS_HELP);
|
|
18968
|
+
return 0;
|
|
18969
|
+
}
|
|
18970
|
+
let root;
|
|
18971
|
+
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
18972
|
+
try {
|
|
18973
|
+
const options = parseDocsArgs(argv);
|
|
18974
|
+
root = resolve8(options.root);
|
|
18975
|
+
if (!isDirectory(root))
|
|
18976
|
+
throw new Error("the root <root> is not an existing directory");
|
|
18977
|
+
if (options.action === "scan")
|
|
18978
|
+
return await runScan(root, options, io);
|
|
18979
|
+
if (options.action === "stub")
|
|
18980
|
+
return await runStub(root, options, io);
|
|
18981
|
+
return await runCheck(root, options, io);
|
|
18982
|
+
} catch (error) {
|
|
18983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18984
|
+
stderr(`repo-tools docs: ${message}
|
|
18985
|
+
`);
|
|
18986
|
+
return error instanceof NothingToMeasure ? 1 : 2;
|
|
18987
|
+
}
|
|
18988
|
+
}
|
|
18989
|
+
|
|
18990
|
+
// src/query/index.ts
|
|
18991
|
+
import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
|
|
18992
|
+
import { resolve as resolve9 } from "node:path";
|
|
18993
|
+
|
|
18994
|
+
// src/query/args.ts
|
|
18995
|
+
var MODE_FLAGS = {
|
|
18996
|
+
"--emit": { name: "emit" },
|
|
18997
|
+
"--check-browser-safety": { name: "check-browser-safety" }
|
|
18998
|
+
};
|
|
18999
|
+
var VALUE_FLAGS3 = new Set(["--root", "--config", "--out", "--node-runtime"]);
|
|
19000
|
+
var COMMAND_USAGE = {
|
|
19001
|
+
dependents: "<file>",
|
|
19002
|
+
"symbol-users": "<symbol>",
|
|
19003
|
+
"is-public": "<pkg> <symbol>",
|
|
19004
|
+
"node-safety": "[pkg]",
|
|
19005
|
+
cycles: ""
|
|
19006
|
+
};
|
|
19007
|
+
function makeCommand(name2, args2) {
|
|
19008
|
+
const usage = COMMAND_USAGE[name2];
|
|
19009
|
+
if (usage === undefined) {
|
|
19010
|
+
throw new Error(`unknown command '${name2}' (see repo-tools query --help)`);
|
|
19011
|
+
}
|
|
19012
|
+
const [a, b] = args2;
|
|
19013
|
+
const need = (count) => {
|
|
19014
|
+
if (args2.length < count)
|
|
19015
|
+
throw new Error(`${name2} needs ${usage}`);
|
|
19016
|
+
};
|
|
19017
|
+
const atMost = (count) => {
|
|
19018
|
+
if (args2.length > count) {
|
|
19019
|
+
throw new Error(count === 0 ? `${name2} takes no more arguments` : `${name2} takes ${usage}, not more arguments`);
|
|
19020
|
+
}
|
|
19021
|
+
};
|
|
19022
|
+
switch (name2) {
|
|
19023
|
+
case "dependents":
|
|
19024
|
+
need(1);
|
|
19025
|
+
atMost(1);
|
|
19026
|
+
return { name: name2, file: a };
|
|
19027
|
+
case "symbol-users":
|
|
19028
|
+
need(1);
|
|
19029
|
+
atMost(1);
|
|
19030
|
+
return { name: name2, symbol: a };
|
|
19031
|
+
case "is-public":
|
|
19032
|
+
need(2);
|
|
19033
|
+
atMost(2);
|
|
19034
|
+
return { name: name2, pkg: a, symbol: b };
|
|
19035
|
+
case "node-safety":
|
|
19036
|
+
atMost(1);
|
|
19037
|
+
return a === undefined ? { name: name2 } : { name: name2, pkg: a };
|
|
19038
|
+
default:
|
|
19039
|
+
atMost(0);
|
|
19040
|
+
return { name: "cycles" };
|
|
19041
|
+
}
|
|
19042
|
+
}
|
|
19043
|
+
function list2(flag, value) {
|
|
19044
|
+
const items = value.split(",");
|
|
19045
|
+
if (items.some((item) => item === ""))
|
|
19046
|
+
throw new Error(`flag ${flag} holds an empty item`);
|
|
19047
|
+
return items;
|
|
19048
|
+
}
|
|
19049
|
+
function parseQueryArgs(argv, cwd) {
|
|
19050
|
+
let root;
|
|
19051
|
+
let config;
|
|
19052
|
+
let out2;
|
|
19053
|
+
let nodeRuntimes;
|
|
19054
|
+
let mode;
|
|
19055
|
+
let components = false;
|
|
19056
|
+
const words = [];
|
|
19057
|
+
for (const arg of argv) {
|
|
19058
|
+
if (!arg.startsWith("-")) {
|
|
19059
|
+
words.push(arg);
|
|
19060
|
+
continue;
|
|
19061
|
+
}
|
|
19062
|
+
const eq = arg.indexOf("=");
|
|
19063
|
+
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
19064
|
+
const value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
19065
|
+
if (name2 === "--components") {
|
|
19066
|
+
if (value !== undefined)
|
|
19067
|
+
throw new Error("flag --components takes no value");
|
|
19068
|
+
components = true;
|
|
19069
|
+
continue;
|
|
19070
|
+
}
|
|
19071
|
+
const modeCommand = MODE_FLAGS[name2];
|
|
19072
|
+
if (modeCommand !== undefined) {
|
|
19073
|
+
if (value !== undefined)
|
|
19074
|
+
throw new Error(`flag ${name2} takes no value`);
|
|
19075
|
+
if (mode !== undefined)
|
|
19076
|
+
throw new Error("use one command or mode in a run");
|
|
19077
|
+
mode = modeCommand;
|
|
19078
|
+
continue;
|
|
19079
|
+
}
|
|
19080
|
+
if (!VALUE_FLAGS3.has(name2)) {
|
|
19081
|
+
throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
|
|
19082
|
+
}
|
|
19083
|
+
if (value === undefined || value === "") {
|
|
19084
|
+
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
19085
|
+
}
|
|
19086
|
+
if (name2 === "--root") {
|
|
19087
|
+
if (root !== undefined)
|
|
19088
|
+
throw new Error("the root is set twice");
|
|
19089
|
+
root = value;
|
|
19090
|
+
} else if (name2 === "--config") {
|
|
19091
|
+
if (isAbsolutePath(value))
|
|
19092
|
+
throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
|
|
19093
|
+
config = value;
|
|
19094
|
+
} else if (name2 === "--out") {
|
|
19095
|
+
if (isAbsolutePath(value))
|
|
19096
|
+
throw new Error(`flag --out ${ABSOLUTE_PATH_TEXT}`);
|
|
19097
|
+
out2 = value;
|
|
19098
|
+
} else {
|
|
19099
|
+
nodeRuntimes = list2(name2, value);
|
|
19100
|
+
}
|
|
19101
|
+
}
|
|
19102
|
+
const [first, ...rest] = words;
|
|
19103
|
+
let command;
|
|
19104
|
+
if (mode !== undefined) {
|
|
19105
|
+
if (first !== undefined) {
|
|
19106
|
+
if (COMMAND_USAGE[first] !== undefined)
|
|
19107
|
+
throw new Error("use one command or mode in a run");
|
|
19108
|
+
throw new Error(`unknown command '${first}' (see repo-tools query --help)`);
|
|
19109
|
+
}
|
|
19110
|
+
command = mode;
|
|
19111
|
+
} else {
|
|
19112
|
+
if (first === undefined) {
|
|
19113
|
+
throw new Error("a command is missing (see repo-tools query --help)");
|
|
19114
|
+
}
|
|
19115
|
+
command = makeCommand(first, rest);
|
|
19116
|
+
}
|
|
19117
|
+
if (components) {
|
|
19118
|
+
if (command.name !== "cycles")
|
|
19119
|
+
throw new Error("flag --components applies to cycles only");
|
|
19120
|
+
command = { name: "cycles", components: true };
|
|
19121
|
+
}
|
|
19122
|
+
return {
|
|
19123
|
+
command,
|
|
19124
|
+
root: root ?? cwd,
|
|
19125
|
+
...config === undefined ? {} : { config },
|
|
19126
|
+
...out2 === undefined ? {} : { out: out2 },
|
|
19127
|
+
...nodeRuntimes === undefined ? {} : { nodeRuntimes }
|
|
19128
|
+
};
|
|
19129
|
+
}
|
|
19130
|
+
|
|
19131
|
+
// src/map/query.ts
|
|
19132
|
+
import { readFileSync as readFileSync24 } from "node:fs";
|
|
19133
|
+
var SUPPORTED_SCHEMA_MAJOR = Number(SCHEMA_VERSION.split(".")[0]);
|
|
19134
|
+
function loadGraph(graphPath) {
|
|
19135
|
+
const data = JSON.parse(readFileSync24(graphPath, "utf8"));
|
|
19136
|
+
if (data === null || typeof data !== "object" || !("modules" in data)) {
|
|
19137
|
+
throw new Error(`${graphPath}: no 'modules' key found -- this is not a map dependency-graph.json, or it is ` + "from an incompatible schema. Cannot answer any query against it.");
|
|
19138
|
+
}
|
|
19139
|
+
const warnings = [];
|
|
19140
|
+
const version = data.metadata?.schemaVersion;
|
|
19141
|
+
if (version === undefined || version === null) {
|
|
19142
|
+
warnings.push(`${graphPath}: metadata.schemaVersion is missing -- proceeding as if it matches this ` + `module's schema (${SCHEMA_VERSION}), but an older or newer artifact whose shape has ` + "actually changed may produce a silently wrong answer.");
|
|
19143
|
+
} else if (Number(String(version).split(".")[0]) !== SUPPORTED_SCHEMA_MAJOR) {
|
|
19144
|
+
throw new Error(`${graphPath}: schemaVersion '${String(version)}' is not compatible with this module's ` + `supported major version ${SUPPORTED_SCHEMA_MAJOR} (SCHEMA_VERSION='${SCHEMA_VERSION}'). ` + "Refusing to query a schema this module was not written against rather than risk a " + "silently wrong answer.");
|
|
19145
|
+
}
|
|
19146
|
+
return { data, warnings };
|
|
19147
|
+
}
|
|
19148
|
+
function entries2(data) {
|
|
19149
|
+
if (!data.modules) {
|
|
19150
|
+
throw new Error("graph data has no 'modules' key -- cannot answer; pass data returned by loadGraph(), or " + "a document with the same shape");
|
|
19151
|
+
}
|
|
19152
|
+
return Object.values(data.modules).flatMap((area) => Object.entries(area));
|
|
19153
|
+
}
|
|
19154
|
+
var sorted = (items) => items.sort(compareCodeUnits);
|
|
19155
|
+
function dependents(data, file) {
|
|
19156
|
+
const all = entries2(data);
|
|
19157
|
+
if (!all.some(([path]) => path === file)) {
|
|
19158
|
+
throw new Error(`'${file}' is not a file in this graph -- cannot answer 'dependents' for an unknown path. ` + "Check the path is repo-relative POSIX and this is the graph for the right repo/checkout. " + "(A file that IS known but genuinely has no importers returns [] normally, not an error.)");
|
|
19159
|
+
}
|
|
19160
|
+
return sorted(all.filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => d.file === file)).map(([path]) => path));
|
|
19161
|
+
}
|
|
19162
|
+
function symbolUsers(data, symbol) {
|
|
19163
|
+
return sorted(entries2(data).filter(([, entry]) => (entry.internalDependencies ?? []).some((d) => (d.imports ?? []).includes(symbol))).map(([path]) => path));
|
|
19164
|
+
}
|
|
19165
|
+
function cycles(data, limits = {}) {
|
|
19166
|
+
const all = entries2(data);
|
|
19167
|
+
const known = new Set(all.map(([path]) => path));
|
|
19168
|
+
const edges = new Map;
|
|
19169
|
+
for (const [path, entry] of all) {
|
|
19170
|
+
const targets = (entry.internalDependencies ?? []).map((d) => d.file).filter((f) => f !== undefined && known.has(f));
|
|
19171
|
+
edges.set(path, [...new Set(targets)]);
|
|
19172
|
+
}
|
|
19173
|
+
const result = simpleCycles(edges, limits);
|
|
19174
|
+
if (result.truncated) {
|
|
19175
|
+
result.warning = `cycles: simple-cycle enumeration hit its safety cap (found ${result.cycles.length} ` + `cycles / ${result.steps} backtracking steps) and stopped early -- the returned list is a ` + "FLOOR (at least this many simple cycles exist), not an exact total.";
|
|
19176
|
+
}
|
|
19177
|
+
return result;
|
|
19178
|
+
}
|
|
19179
|
+
|
|
19180
|
+
// src/query/graph.ts
|
|
19181
|
+
function fileEntriesOf(graph) {
|
|
19182
|
+
const pairs = [];
|
|
19183
|
+
for (const files of Object.values(graph.modules)) {
|
|
19184
|
+
for (const [file, entry] of Object.entries(files))
|
|
19185
|
+
pairs.push([file, entry]);
|
|
19186
|
+
}
|
|
19187
|
+
return pairs;
|
|
19188
|
+
}
|
|
19189
|
+
function buildForward(fileEntries, allFiles) {
|
|
19190
|
+
const forward = new Map;
|
|
19191
|
+
for (const [file, entry] of fileEntries) {
|
|
19192
|
+
const targets = new Set;
|
|
19193
|
+
for (const dep of entry.internalDependencies ?? []) {
|
|
19194
|
+
if (dep.file !== undefined && allFiles.has(dep.file))
|
|
19195
|
+
targets.add(dep.file);
|
|
19196
|
+
}
|
|
19197
|
+
forward.set(file, targets);
|
|
19198
|
+
}
|
|
19199
|
+
return forward;
|
|
19200
|
+
}
|
|
19201
|
+
function invert(forward) {
|
|
19202
|
+
const reverse = new Map;
|
|
19203
|
+
for (const [file, targets] of forward) {
|
|
19204
|
+
for (const target of targets) {
|
|
19205
|
+
const importers = reverse.get(target) ?? new Set;
|
|
19206
|
+
importers.add(file);
|
|
19207
|
+
reverse.set(target, importers);
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
19210
|
+
const out2 = {};
|
|
19211
|
+
for (const target of sortCodeUnits([...reverse.keys()])) {
|
|
19212
|
+
out2[target] = sortCodeUnits([...reverse.get(target) ?? []]);
|
|
19213
|
+
}
|
|
19214
|
+
return out2;
|
|
19215
|
+
}
|
|
19216
|
+
|
|
19217
|
+
// src/query/load.ts
|
|
19218
|
+
import { existsSync as existsSync11, readFileSync as readFileSync25 } from "node:fs";
|
|
19219
|
+
var RUN_MAP = "run repo-tools map first";
|
|
19220
|
+
function reportRel(out2, name2) {
|
|
19221
|
+
return `${out2.replace(/\\/g, "/").replace(/\/+$/, "")}/${name2}`;
|
|
17689
19222
|
}
|
|
17690
19223
|
function loadQueryInput(root, out2) {
|
|
17691
19224
|
const rel = reportRel(out2, "dependency-graph.json");
|
|
17692
19225
|
const shown = `the dependency graph <root>/${rel}`;
|
|
17693
19226
|
const path = resolveUnderRoot(root, rel);
|
|
17694
|
-
if (!
|
|
19227
|
+
if (!existsSync11(path))
|
|
17695
19228
|
throw new Error(`${shown} does not exist; ${RUN_MAP}`);
|
|
17696
19229
|
let loaded;
|
|
17697
19230
|
try {
|
|
@@ -17728,13 +19261,13 @@ function loadSurfaces(input) {
|
|
|
17728
19261
|
const rel = reportRel(input.out, "package-export-surfaces.json");
|
|
17729
19262
|
const shown = `the export-surfaces report <root>/${rel}`;
|
|
17730
19263
|
const path = resolveUnderRoot(input.root, rel);
|
|
17731
|
-
if (!
|
|
19264
|
+
if (!existsSync11(path)) {
|
|
17732
19265
|
const why = input.language === "csharp" ? " (map writes none for C#)" : `; ${RUN_MAP}`;
|
|
17733
19266
|
throw new Error(`${shown} does not exist${why}`);
|
|
17734
19267
|
}
|
|
17735
19268
|
let parsed;
|
|
17736
19269
|
try {
|
|
17737
|
-
parsed = JSON.parse(
|
|
19270
|
+
parsed = JSON.parse(readFileSync25(path, "utf8"));
|
|
17738
19271
|
} catch {
|
|
17739
19272
|
throw new Error(`${shown} is not valid JSON; ${RUN_MAP}`);
|
|
17740
19273
|
}
|
|
@@ -18033,13 +19566,13 @@ report, a path that is not a file of the graph, a browser-safety command on a
|
|
|
18033
19566
|
graph that is not TypeScript/JavaScript, or a browser-safety leak with
|
|
18034
19567
|
--check-browser-safety.
|
|
18035
19568
|
`;
|
|
18036
|
-
async function
|
|
19569
|
+
async function run6(argv, io) {
|
|
18037
19570
|
let root;
|
|
18038
19571
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
18039
19572
|
try {
|
|
18040
19573
|
const options = parseQueryArgs(argv, process.cwd());
|
|
18041
|
-
root =
|
|
18042
|
-
if (!(
|
|
19574
|
+
root = resolve9(options.root);
|
|
19575
|
+
if (!(existsSync12(root) && statSync10(root).isDirectory())) {
|
|
18043
19576
|
throw new Error("the root <root> is not an existing directory");
|
|
18044
19577
|
}
|
|
18045
19578
|
const config = mergeQueryConfig({ out: options.out, nodeRuntimes: options.nodeRuntimes }, loadConfigSections(root, options.config));
|
|
@@ -18073,40 +19606,8 @@ async function run5(argv, io) {
|
|
|
18073
19606
|
}
|
|
18074
19607
|
|
|
18075
19608
|
// src/ste/index.ts
|
|
18076
|
-
import { existsSync as
|
|
18077
|
-
import { basename as
|
|
18078
|
-
|
|
18079
|
-
// src/ste/rules.ts
|
|
18080
|
-
var IRREGULAR_PARTICIPLES = "read|built|sent|held|kept|made|found|put|set|run|lost|left|met|paid|told" + "|thrown|drawn|given|taken|shown|known|grown|blown" + "|cut|meant|dealt|felt|sold|said|spent|split|shut|hit|let|bred|fed|led";
|
|
18081
|
-
var DETERMINERS = "the|a|an|its|their|this|that|our|your|each|every" + "|his|her|my|these|those";
|
|
18082
|
-
var PASSIVE_SOURCE = `${B}(?:${ci("is|are|was|were|be|been|being")})${S}+` + `(?:${W}+(?:${ci("ed|en")})|(?:${ci(IRREGULAR_PARTICIPLES)}))${S}+` + `by${S}+(?:(?:${ci(DETERMINERS)})${B}|[A-Z\`])`;
|
|
18083
|
-
var AMBIGUOUS_VERBS = "is|are|was|were|be|will|can|may|must|does|do|has|have|should|shall" + "|disables?|enables?|causes?|returns?|means?|allows?|prevents?|makes?" + "|sets?|gets?|adds?|removes?|creates?|requires?|affects?|applies|happens?";
|
|
18084
|
-
var AMBIGUOUS_SOURCE = `(?:^|(?<=[.!?,;:])${S})${S}*(?:(?:${ci("and|but|or|so|then|yet")})${S}+)?` + `(${ci("it|this|that|these|those")})${S}+(?:${AMBIGUOUS_VERBS})${B}`;
|
|
18085
|
-
var WORDY = [
|
|
18086
|
-
["in order to", "to"],
|
|
18087
|
-
["at this point in time", "now"],
|
|
18088
|
-
["in the event that", "if"],
|
|
18089
|
-
["due to the fact that", "because"],
|
|
18090
|
-
["a number of", "some"],
|
|
18091
|
-
["utilise", "use"],
|
|
18092
|
-
["utilize", "use"],
|
|
18093
|
-
["prior to", "before"],
|
|
18094
|
-
["subsequent to", "after"],
|
|
18095
|
-
["in the case of", "for"],
|
|
18096
|
-
["in spite of the fact that", "although"],
|
|
18097
|
-
["for the purpose of", "to"],
|
|
18098
|
-
["has the ability to", "can"],
|
|
18099
|
-
["is able to", "can"]
|
|
18100
|
-
];
|
|
18101
|
-
var IDENTIFIER_SOURCE = "`[^`]*`" + `|${B}${W}+\\(\\)` + `|${B}${W}*[a-z]${W}*[A-Z]${W}*${B}` + `|${B}${W}+_${W}+${B}` + `|${B}${W}+\\.${W}+${B}`;
|
|
18102
|
-
var MAX_WORDS_PROCEDURAL = 20;
|
|
18103
|
-
var MAX_WORDS_DESCRIPTIVE = 25;
|
|
18104
|
-
function rule(source, flags2 = "") {
|
|
18105
|
-
return new RegExp(source, `u${flags2}`);
|
|
18106
|
-
}
|
|
18107
|
-
function stripIdentifiers(text) {
|
|
18108
|
-
return text.replace(rule(IDENTIFIER_SOURCE, "g"), " ");
|
|
18109
|
-
}
|
|
19609
|
+
import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
|
|
19610
|
+
import { basename as basename10, join as join28 } from "node:path";
|
|
18110
19611
|
|
|
18111
19612
|
// src/ste/markdown.ts
|
|
18112
19613
|
function stripNoise(text) {
|
|
@@ -18149,10 +19650,10 @@ function proseOnly(line) {
|
|
|
18149
19650
|
}
|
|
18150
19651
|
var EMPHASIS = /\*\*|__/gu;
|
|
18151
19652
|
var DECIMAL2 = rule(`(${D})\\.(${D})`, "g");
|
|
18152
|
-
var
|
|
19653
|
+
var SENTENCE_END2 = rule(`(?<=[.!?])${S}+`);
|
|
18153
19654
|
function sentences(block) {
|
|
18154
19655
|
const guarded = block.replace(EMPHASIS, "").replace(DECIMAL2, "$1<DOT>$2");
|
|
18155
|
-
return guarded.split(
|
|
19656
|
+
return guarded.split(SENTENCE_END2).map((part) => part.replaceAll("<DOT>", "."));
|
|
18156
19657
|
}
|
|
18157
19658
|
var CODE_SPAN = /`[^`]*`/gu;
|
|
18158
19659
|
var EMPHASIS_CHARS = /[*_]/gu;
|
|
@@ -18161,7 +19662,7 @@ function wordCount(sentence) {
|
|
|
18161
19662
|
const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
|
|
18162
19663
|
return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
|
|
18163
19664
|
}
|
|
18164
|
-
function
|
|
19665
|
+
function join27(parts2, start2) {
|
|
18165
19666
|
const chunks = [];
|
|
18166
19667
|
const lineMap = [];
|
|
18167
19668
|
parts2.forEach(([n, body2], i2) => {
|
|
@@ -18181,8 +19682,8 @@ function lineAt2(index, lineMap) {
|
|
|
18181
19682
|
var LIST_ITEM = rule(`^${S}*(?:[-*+]|${D}+\\.)${S}`);
|
|
18182
19683
|
var PROCEDURAL_STARTS = ["- [", "1.", "2.", "3.", "4.", "5.", "6."];
|
|
18183
19684
|
var WORDY_RULES = WORDY.map(([phrase, better]) => [phrase, better, rule(`(?<![\\p{L}\\p{N}_-])${reEscape(phrase)}${B}`, "g")]);
|
|
18184
|
-
var
|
|
18185
|
-
var
|
|
19685
|
+
var AMBIGUOUS2 = rule(AMBIGUOUS_SOURCE, "g");
|
|
19686
|
+
var PASSIVE2 = rule(PASSIVE_SOURCE, "g");
|
|
18186
19687
|
function paragraphs(text) {
|
|
18187
19688
|
const blocks = [];
|
|
18188
19689
|
let para = [];
|
|
@@ -18191,7 +19692,7 @@ function paragraphs(text) {
|
|
|
18191
19692
|
for (const [n, line] of stripNoise(text)) {
|
|
18192
19693
|
const startsItem = LIST_ITEM.test(line);
|
|
18193
19694
|
if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
|
|
18194
|
-
blocks.push(
|
|
19695
|
+
blocks.push(join27(para, paraStart));
|
|
18195
19696
|
para = [];
|
|
18196
19697
|
}
|
|
18197
19698
|
if (para.length === 0)
|
|
@@ -18200,7 +19701,7 @@ function paragraphs(text) {
|
|
|
18200
19701
|
prevLine = n;
|
|
18201
19702
|
}
|
|
18202
19703
|
if (para.length > 0)
|
|
18203
|
-
blocks.push(
|
|
19704
|
+
blocks.push(join27(para, paraStart));
|
|
18204
19705
|
return blocks;
|
|
18205
19706
|
}
|
|
18206
19707
|
function checkMarkdown(name2, text) {
|
|
@@ -18224,75 +19725,16 @@ function checkMarkdown(name2, text) {
|
|
|
18224
19725
|
problems.push(`${name2}:${lineAt2(m.index, lineMap)}: wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`);
|
|
18225
19726
|
}
|
|
18226
19727
|
}
|
|
18227
|
-
for (const m of bare.matchAll(
|
|
19728
|
+
for (const m of bare.matchAll(AMBIGUOUS2)) {
|
|
18228
19729
|
problems.push(`${name2}:${lineAt2(m.index, lineMap)}: ambiguous reference: ${pyRepr(pyStrip(m[0]))}`);
|
|
18229
19730
|
}
|
|
18230
|
-
for (const m of bare.matchAll(
|
|
19731
|
+
for (const m of bare.matchAll(PASSIVE2)) {
|
|
18231
19732
|
problems.push(`${name2}:${lineAt2(m.index, lineMap)}: passive voice: ${pyRepr(pyStrip(m[0]))}`);
|
|
18232
19733
|
}
|
|
18233
19734
|
}
|
|
18234
19735
|
return problems;
|
|
18235
19736
|
}
|
|
18236
19737
|
|
|
18237
|
-
// src/ste/prose.ts
|
|
18238
|
-
var PASSIVE2 = rule(PASSIVE_SOURCE);
|
|
18239
|
-
var AMBIGUOUS2 = rule(AMBIGUOUS_SOURCE);
|
|
18240
|
-
var SENTENCE_END2 = rule(`(?<=[.!?])${S}+`);
|
|
18241
|
-
function sentencesOf(text) {
|
|
18242
|
-
const collapsed = pySplit(text).join(" ");
|
|
18243
|
-
return collapsed.split(SENTENCE_END2).map(pyStrip).filter((s) => s !== "");
|
|
18244
|
-
}
|
|
18245
|
-
function clip(text, limit = 60) {
|
|
18246
|
-
return codePointLength(text) <= limit ? text : `${headCodePoints(text, limit - 1)}…`;
|
|
18247
|
-
}
|
|
18248
|
-
function checkProse(text) {
|
|
18249
|
-
const findings = [];
|
|
18250
|
-
for (const sentence of sentencesOf(text)) {
|
|
18251
|
-
const words = pySplit(sentence);
|
|
18252
|
-
if (words.length > MAX_WORDS_PROCEDURAL) {
|
|
18253
|
-
findings.push([
|
|
18254
|
-
"STE-LEN",
|
|
18255
|
-
`sentence has ${words.length} words (limit ${MAX_WORDS_PROCEDURAL}): '${clip(sentence)}'`
|
|
18256
|
-
]);
|
|
18257
|
-
}
|
|
18258
|
-
const passive = PASSIVE2.exec(sentence);
|
|
18259
|
-
if (passive) {
|
|
18260
|
-
findings.push(["STE-VOICE", `possible passive voice '${passive[0]}' -- prefer active`]);
|
|
18261
|
-
}
|
|
18262
|
-
const ambiguous = AMBIGUOUS2.exec(sentence);
|
|
18263
|
-
if (ambiguous) {
|
|
18264
|
-
findings.push(["STE-REF", `ambiguous '${ambiguous[1]}' -- name the thing it refers to`]);
|
|
18265
|
-
}
|
|
18266
|
-
}
|
|
18267
|
-
const prose = stripIdentifiers(text);
|
|
18268
|
-
for (const [phrase, better] of WORDY) {
|
|
18269
|
-
if (rule(`${B}${ci(reEscape(phrase))}${B}`).test(prose)) {
|
|
18270
|
-
findings.push(["STE-WORD", `'${phrase}' -- prefer '${better}'`]);
|
|
18271
|
-
}
|
|
18272
|
-
}
|
|
18273
|
-
return findings;
|
|
18274
|
-
}
|
|
18275
|
-
var SECTION_HEADER = /^(Args|Arguments|Returns|Raises|Yields|Examples|Attributes|Parameters|Notes?|See Also):?$/u;
|
|
18276
|
-
function proseOf(doc) {
|
|
18277
|
-
const out2 = [];
|
|
18278
|
-
let inFence = false;
|
|
18279
|
-
for (const raw of pySplitlines(doc)) {
|
|
18280
|
-
const line = pyStrip(pyStrip(raw).replace(/^\*+/, ""));
|
|
18281
|
-
if (line.startsWith("```")) {
|
|
18282
|
-
inFence = !inFence;
|
|
18283
|
-
continue;
|
|
18284
|
-
}
|
|
18285
|
-
if (inFence || line.startsWith("@") || line.startsWith(">>>"))
|
|
18286
|
-
continue;
|
|
18287
|
-
if (SECTION_HEADER.test(line))
|
|
18288
|
-
continue;
|
|
18289
|
-
if (/^-{3,}$/u.test(line))
|
|
18290
|
-
continue;
|
|
18291
|
-
out2.push(line);
|
|
18292
|
-
}
|
|
18293
|
-
return out2.filter((p) => p !== "").join(" ");
|
|
18294
|
-
}
|
|
18295
|
-
|
|
18296
19738
|
// src/ste/index.ts
|
|
18297
19739
|
var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...]
|
|
18298
19740
|
repo-tools ste --prose <file>
|
|
@@ -18321,7 +19763,7 @@ or when a file is not UTF-8.
|
|
|
18321
19763
|
`;
|
|
18322
19764
|
var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
|
|
18323
19765
|
function readText(path) {
|
|
18324
|
-
const bytes =
|
|
19766
|
+
const bytes = readFileSync26(path);
|
|
18325
19767
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
18326
19768
|
return text.replace(/\r\n?/g, `
|
|
18327
19769
|
`);
|
|
@@ -18335,36 +19777,36 @@ function comparePaths(a, b) {
|
|
|
18335
19777
|
return a.length - b.length;
|
|
18336
19778
|
}
|
|
18337
19779
|
function targetsFor(path) {
|
|
18338
|
-
if (
|
|
19780
|
+
if (existsSync13(path) && statSync11(path).isFile())
|
|
18339
19781
|
return [path];
|
|
18340
19782
|
const found = [];
|
|
18341
19783
|
const walk = (dir, parts2) => {
|
|
18342
19784
|
let entries;
|
|
18343
19785
|
try {
|
|
18344
|
-
entries =
|
|
19786
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
18345
19787
|
} catch {
|
|
18346
19788
|
return;
|
|
18347
19789
|
}
|
|
18348
19790
|
for (const entry of entries) {
|
|
18349
|
-
const full =
|
|
19791
|
+
const full = join28(dir, entry.name);
|
|
18350
19792
|
if (entry.isDirectory()) {
|
|
18351
19793
|
if (entry.name !== ".pytest_cache")
|
|
18352
19794
|
walk(full, [...parts2, entry.name]);
|
|
18353
19795
|
} else if (entry.name.endsWith(".md")) {
|
|
18354
|
-
const isFile = entry.isFile() || entry.isSymbolicLink() &&
|
|
19796
|
+
const isFile = entry.isFile() || entry.isSymbolicLink() && statSync11(full).isFile();
|
|
18355
19797
|
if (isFile)
|
|
18356
19798
|
found.push([...parts2, entry.name]);
|
|
18357
19799
|
}
|
|
18358
19800
|
}
|
|
18359
19801
|
};
|
|
18360
|
-
if (
|
|
19802
|
+
if (existsSync13(path) && lstatSync6(path).isDirectory())
|
|
18361
19803
|
walk(path, []);
|
|
18362
|
-
return found.sort(comparePaths).map((parts2) =>
|
|
19804
|
+
return found.sort(comparePaths).map((parts2) => join28(path, ...parts2));
|
|
18363
19805
|
}
|
|
18364
19806
|
function isHistoricalRecord(path) {
|
|
18365
19807
|
let text;
|
|
18366
19808
|
try {
|
|
18367
|
-
text = new TextDecoder("utf-8").decode(
|
|
19809
|
+
text = new TextDecoder("utf-8").decode(readFileSync26(path));
|
|
18368
19810
|
} catch {
|
|
18369
19811
|
return false;
|
|
18370
19812
|
}
|
|
@@ -18381,7 +19823,7 @@ function runProse(args2, io) {
|
|
|
18381
19823
|
try {
|
|
18382
19824
|
text = readText(file);
|
|
18383
19825
|
} catch {
|
|
18384
|
-
io.stderr(`repo-tools ste: cannot read ${
|
|
19826
|
+
io.stderr(`repo-tools ste: cannot read ${basename10(file)} as UTF-8 text
|
|
18385
19827
|
`);
|
|
18386
19828
|
return 2;
|
|
18387
19829
|
}
|
|
@@ -18390,7 +19832,7 @@ function runProse(args2, io) {
|
|
|
18390
19832
|
`);
|
|
18391
19833
|
return 0;
|
|
18392
19834
|
}
|
|
18393
|
-
async function
|
|
19835
|
+
async function run7(argv, io) {
|
|
18394
19836
|
if (argv[0] === "--prose")
|
|
18395
19837
|
return runProse(argv.slice(1), io);
|
|
18396
19838
|
if (argv.length === 0) {
|
|
@@ -18403,7 +19845,7 @@ async function run6(argv, io) {
|
|
|
18403
19845
|
let skipped = 0;
|
|
18404
19846
|
for (const arg of argv) {
|
|
18405
19847
|
for (const target of targetsFor(arg)) {
|
|
18406
|
-
const name2 =
|
|
19848
|
+
const name2 = basename10(target);
|
|
18407
19849
|
if (isHistoricalRecord(target)) {
|
|
18408
19850
|
skipped += 1;
|
|
18409
19851
|
io.stdout(`
|
|
@@ -18462,6 +19904,11 @@ ${MAP_HELP}`,
|
|
|
18462
19904
|
help: CHECK_HELP,
|
|
18463
19905
|
run: run2
|
|
18464
19906
|
},
|
|
19907
|
+
docs: {
|
|
19908
|
+
summary: "Measure, stub and gate the doc comments of TypeScript, JavaScript and Python source.",
|
|
19909
|
+
help: DOCS_HELP,
|
|
19910
|
+
run: run5
|
|
19911
|
+
},
|
|
18465
19912
|
chunk: {
|
|
18466
19913
|
summary: "Split a large file into chunks, merge the chunks back, or show changed chunks.",
|
|
18467
19914
|
help: CHUNK_HELP,
|
|
@@ -18475,12 +19922,12 @@ ${MAP_HELP}`,
|
|
|
18475
19922
|
query: {
|
|
18476
19923
|
summary: "Answer structural questions from the depgraph reports, or write derived reports.",
|
|
18477
19924
|
help: QUERY_HELP,
|
|
18478
|
-
run:
|
|
19925
|
+
run: run6
|
|
18479
19926
|
},
|
|
18480
19927
|
ste: {
|
|
18481
19928
|
summary: "Check Markdown or docstring prose against Simplified Technical English.",
|
|
18482
19929
|
help: STE_HELP,
|
|
18483
|
-
run:
|
|
19930
|
+
run: run7
|
|
18484
19931
|
}
|
|
18485
19932
|
};
|
|
18486
19933
|
var SUBCOMMANDS = Object.keys(REGISTRY);
|