@danielsimonjr/repo-tools 2.3.0 → 2.4.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 +79 -0
- package/README.md +26 -3
- package/dist/cli.js +1331 -1174
- package/package.json +1 -1
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.4.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",
|
|
@@ -55,10 +55,10 @@ var package_default = {
|
|
|
55
55
|
|
|
56
56
|
// src/check/index.ts
|
|
57
57
|
import { readdirSync as readdirSync2, readFileSync as readFileSync17 } from "node:fs";
|
|
58
|
-
import { join as
|
|
58
|
+
import { join as join21, resolve as resolve6 } from "node:path";
|
|
59
59
|
|
|
60
60
|
// src/config.ts
|
|
61
|
-
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
61
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
62
62
|
import { posix, resolve, win32 } from "node:path";
|
|
63
63
|
|
|
64
64
|
// src/depgraph/api-surface.ts
|
|
@@ -662,13 +662,231 @@ function buildApiSurfaceReport(surface, files, stabilityTags) {
|
|
|
662
662
|
};
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
-
// src/depgraph/
|
|
666
|
-
import {
|
|
665
|
+
// src/depgraph/duplicates.ts
|
|
666
|
+
import { readFileSync } from "node:fs";
|
|
667
667
|
import { join as join2 } from "node:path";
|
|
668
668
|
|
|
669
|
-
// src/
|
|
670
|
-
|
|
671
|
-
|
|
669
|
+
// src/mask.ts
|
|
670
|
+
var REGEX_KEYWORDS2 = new Set([
|
|
671
|
+
"return",
|
|
672
|
+
"typeof",
|
|
673
|
+
"instanceof",
|
|
674
|
+
"in",
|
|
675
|
+
"of",
|
|
676
|
+
"new",
|
|
677
|
+
"delete",
|
|
678
|
+
"void",
|
|
679
|
+
"throw",
|
|
680
|
+
"case",
|
|
681
|
+
"do",
|
|
682
|
+
"else",
|
|
683
|
+
"yield",
|
|
684
|
+
"await"
|
|
685
|
+
]);
|
|
686
|
+
function regexCanStart(src, prev, afterStmtParen) {
|
|
687
|
+
if (prev < 0)
|
|
688
|
+
return true;
|
|
689
|
+
const c = src[prev] ?? "";
|
|
690
|
+
if (c === ")")
|
|
691
|
+
return afterStmtParen;
|
|
692
|
+
if (c === ">")
|
|
693
|
+
return src[prev - 1] === "=";
|
|
694
|
+
if ((c === "+" || c === "-") && src[prev - 1] === c)
|
|
695
|
+
return false;
|
|
696
|
+
if (/[([{},;:?=!&|+\-*%^~]/.test(c))
|
|
697
|
+
return true;
|
|
698
|
+
if (!/[\w$]/.test(c))
|
|
699
|
+
return false;
|
|
700
|
+
const word = /[\w$]+$/.exec(src.slice(Math.max(0, prev - 15), prev + 1))?.[0] ?? "";
|
|
701
|
+
return REGEX_KEYWORDS2.has(word) && !/[\w$.]/.test(src[prev - word.length] ?? "");
|
|
702
|
+
}
|
|
703
|
+
function regexBodyEnd(src, start2) {
|
|
704
|
+
let inClass = false;
|
|
705
|
+
for (let j = start2 + 1;j < src.length; j++) {
|
|
706
|
+
const c = src[j];
|
|
707
|
+
if (c === `
|
|
708
|
+
` || c === "\r")
|
|
709
|
+
return -1;
|
|
710
|
+
if (c === "\\")
|
|
711
|
+
j += 1;
|
|
712
|
+
else if (c === "[")
|
|
713
|
+
inClass = true;
|
|
714
|
+
else if (c === "]")
|
|
715
|
+
inClass = false;
|
|
716
|
+
else if (c === "/" && !inClass)
|
|
717
|
+
return j;
|
|
718
|
+
}
|
|
719
|
+
return -1;
|
|
720
|
+
}
|
|
721
|
+
function scan(src) {
|
|
722
|
+
const out2 = [];
|
|
723
|
+
const push = (kind, start2, end) => {
|
|
724
|
+
if (end <= start2)
|
|
725
|
+
return;
|
|
726
|
+
const last = out2[out2.length - 1];
|
|
727
|
+
if (last && last.kind === kind && last.end === start2)
|
|
728
|
+
last.end = end;
|
|
729
|
+
else
|
|
730
|
+
out2.push({ kind, start: start2, end });
|
|
731
|
+
};
|
|
732
|
+
const templates = [];
|
|
733
|
+
let i2 = 0;
|
|
734
|
+
const n = src.length;
|
|
735
|
+
const readTemplateBody = () => {
|
|
736
|
+
const start2 = i2;
|
|
737
|
+
while (i2 < n) {
|
|
738
|
+
const c = src[i2];
|
|
739
|
+
if (c === "\\") {
|
|
740
|
+
i2 += 2;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (c === "`") {
|
|
744
|
+
push("string", start2, i2);
|
|
745
|
+
push("code", i2, i2 + 1);
|
|
746
|
+
i2 += 1;
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (c === "$" && src[i2 + 1] === "{") {
|
|
750
|
+
push("string", start2, i2);
|
|
751
|
+
push("code", i2, i2 + 2);
|
|
752
|
+
i2 += 2;
|
|
753
|
+
templates.push(0);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
i2 += 1;
|
|
757
|
+
}
|
|
758
|
+
push("string", start2, n);
|
|
759
|
+
};
|
|
760
|
+
let codeStart = 0;
|
|
761
|
+
let prev = -1;
|
|
762
|
+
const parenStmt = [];
|
|
763
|
+
let closedStmt = false;
|
|
764
|
+
const wordEndingAt = (index) => {
|
|
765
|
+
let end = index;
|
|
766
|
+
while (end > 0) {
|
|
767
|
+
const prev = src[end - 1] ?? "";
|
|
768
|
+
if (/\s/.test(prev)) {
|
|
769
|
+
end -= 1;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (prev === "/" && src[end - 2] === "*") {
|
|
773
|
+
const open = src.lastIndexOf("/*", end - 2);
|
|
774
|
+
if (open === -1)
|
|
775
|
+
break;
|
|
776
|
+
end = open;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
|
|
782
|
+
};
|
|
783
|
+
while (i2 < n) {
|
|
784
|
+
const c = src[i2];
|
|
785
|
+
const next = src[i2 + 1];
|
|
786
|
+
if (c === "/" && next !== "/" && next !== "*" && regexCanStart(src, prev, closedStmt)) {
|
|
787
|
+
const close = regexBodyEnd(src, i2);
|
|
788
|
+
if (close !== -1) {
|
|
789
|
+
push("code", codeStart, i2 + 1);
|
|
790
|
+
push("string", i2 + 1, close);
|
|
791
|
+
i2 = close + 1;
|
|
792
|
+
while (i2 < n && /[a-z]/i.test(src[i2] ?? ""))
|
|
793
|
+
i2 += 1;
|
|
794
|
+
push("code", close, i2);
|
|
795
|
+
prev = i2 - 1;
|
|
796
|
+
closedStmt = false;
|
|
797
|
+
codeStart = i2;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (c === "/" && next === "/") {
|
|
802
|
+
push("code", codeStart, i2);
|
|
803
|
+
const eol = src.indexOf(`
|
|
804
|
+
`, i2);
|
|
805
|
+
const end = eol === -1 ? n : eol;
|
|
806
|
+
push("comment", i2, end);
|
|
807
|
+
i2 = end;
|
|
808
|
+
codeStart = i2;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (c === "/" && next === "*") {
|
|
812
|
+
push("code", codeStart, i2);
|
|
813
|
+
const close = src.indexOf("*/", i2 + 2);
|
|
814
|
+
const end = close === -1 ? n : close + 2;
|
|
815
|
+
push("comment", i2, end);
|
|
816
|
+
i2 = end;
|
|
817
|
+
codeStart = i2;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (c === "'" || c === '"') {
|
|
821
|
+
push("code", codeStart, i2 + 1);
|
|
822
|
+
let j = i2 + 1;
|
|
823
|
+
while (j < n && src[j] !== c && src[j] !== `
|
|
824
|
+
`)
|
|
825
|
+
j += src[j] === "\\" ? 2 : 1;
|
|
826
|
+
const end = Math.min(j, n);
|
|
827
|
+
push("string", i2 + 1, end);
|
|
828
|
+
i2 = end < n && src[end] === c ? end + 1 : end;
|
|
829
|
+
push("code", end, i2);
|
|
830
|
+
prev = i2 - 1;
|
|
831
|
+
closedStmt = false;
|
|
832
|
+
codeStart = i2;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (c === "`") {
|
|
836
|
+
push("code", codeStart, i2 + 1);
|
|
837
|
+
i2 += 1;
|
|
838
|
+
readTemplateBody();
|
|
839
|
+
prev = i2 - 1;
|
|
840
|
+
closedStmt = false;
|
|
841
|
+
codeStart = i2;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (!/\s/.test(c ?? "")) {
|
|
845
|
+
if (c === "(") {
|
|
846
|
+
const word = wordEndingAt(i2);
|
|
847
|
+
parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
|
|
848
|
+
closedStmt = false;
|
|
849
|
+
} else if (c === ")")
|
|
850
|
+
closedStmt = parenStmt.pop() === true;
|
|
851
|
+
else
|
|
852
|
+
closedStmt = false;
|
|
853
|
+
prev = i2;
|
|
854
|
+
}
|
|
855
|
+
if (templates.length > 0) {
|
|
856
|
+
const depth = templates.length - 1;
|
|
857
|
+
if (c === "{")
|
|
858
|
+
templates[depth] = (templates[depth] ?? 0) + 1;
|
|
859
|
+
else if (c === "}") {
|
|
860
|
+
if (templates[depth] === 0) {
|
|
861
|
+
templates.pop();
|
|
862
|
+
push("code", codeStart, i2 + 1);
|
|
863
|
+
i2 += 1;
|
|
864
|
+
readTemplateBody();
|
|
865
|
+
prev = i2 - 1;
|
|
866
|
+
closedStmt = false;
|
|
867
|
+
codeStart = i2;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
templates[depth] = (templates[depth] ?? 1) - 1;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
i2 += 1;
|
|
874
|
+
}
|
|
875
|
+
push("code", codeStart, n);
|
|
876
|
+
return out2;
|
|
877
|
+
}
|
|
878
|
+
function blank(text) {
|
|
879
|
+
return text.replace(/[^\r\n]/g, " ");
|
|
880
|
+
}
|
|
881
|
+
function blankCommentsAndStrings(src) {
|
|
882
|
+
return scan(src).map((s) => {
|
|
883
|
+
const text = src.slice(s.start, s.end);
|
|
884
|
+
return s.kind === "code" ? text : blank(text);
|
|
885
|
+
}).join("");
|
|
886
|
+
}
|
|
887
|
+
function stripComments(src) {
|
|
888
|
+
return scan(src).filter((s) => s.kind !== "comment").map((s) => src.slice(s.start, s.end)).join("");
|
|
889
|
+
}
|
|
672
890
|
|
|
673
891
|
// src/sort.ts
|
|
674
892
|
function compareCodeUnits(a, b) {
|
|
@@ -680,79 +898,17 @@ function sortCodeUnits(values) {
|
|
|
680
898
|
return [...values].sort(compareCodeUnits);
|
|
681
899
|
}
|
|
682
900
|
|
|
683
|
-
// src/depgraph/
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
function listNames(dir) {
|
|
689
|
-
return sortCodeUnits(readdirHook.names(dir));
|
|
901
|
+
// src/depgraph/paths.ts
|
|
902
|
+
import { join, relative } from "node:path";
|
|
903
|
+
var OUTPUT_SUBDIR = "docs/architecture";
|
|
904
|
+
function withoutTsExtension(path) {
|
|
905
|
+
return path.replace(/\.tsx?$/, "");
|
|
690
906
|
}
|
|
691
|
-
function
|
|
692
|
-
return
|
|
907
|
+
function toPosix(path) {
|
|
908
|
+
return path.replace(/\\/g, "/");
|
|
693
909
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
let link;
|
|
697
|
-
try {
|
|
698
|
-
link = lstatSync(path).isSymbolicLink();
|
|
699
|
-
} catch {
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
if (link)
|
|
703
|
-
linkLog.skipped.add(path);
|
|
704
|
-
return link;
|
|
705
|
-
}
|
|
706
|
-
function isLinkEntry(dir, entry) {
|
|
707
|
-
if (!entry.isSymbolicLink())
|
|
708
|
-
return false;
|
|
709
|
-
linkLog.skipped.add(join(dir, entry.name));
|
|
710
|
-
return true;
|
|
711
|
-
}
|
|
712
|
-
function isWalkable(dir) {
|
|
713
|
-
return existsSync(dir) && !isLink(dir);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// src/depgraph/exclude.ts
|
|
717
|
-
var DEFAULT_EXCLUDE = [
|
|
718
|
-
"node_modules",
|
|
719
|
-
"dist",
|
|
720
|
-
"build",
|
|
721
|
-
"coverage",
|
|
722
|
-
".git"
|
|
723
|
-
];
|
|
724
|
-
var TEST_DIR_NAMES = ["test", "tests"];
|
|
725
|
-
function isSourceInput(name2) {
|
|
726
|
-
return (name2.endsWith(".ts") || name2.endsWith(".tsx")) && !name2.endsWith(".d.ts") && !/\.(test|spec)\.tsx?$/.test(name2);
|
|
727
|
-
}
|
|
728
|
-
function sourceTsFilesUnder(dir, skip = new Set(DEFAULT_EXCLUDE), files = []) {
|
|
729
|
-
if (!isWalkable(dir))
|
|
730
|
-
return files;
|
|
731
|
-
for (const entry of listNames(dir)) {
|
|
732
|
-
if (skip.has(entry))
|
|
733
|
-
continue;
|
|
734
|
-
const fullPath = join2(dir, entry);
|
|
735
|
-
if (isLink(fullPath))
|
|
736
|
-
continue;
|
|
737
|
-
if (statSync(fullPath).isDirectory())
|
|
738
|
-
sourceTsFilesUnder(fullPath, skip, files);
|
|
739
|
-
else if (isSourceInput(entry))
|
|
740
|
-
files.push(fullPath);
|
|
741
|
-
}
|
|
742
|
-
return files;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// src/depgraph/paths.ts
|
|
746
|
-
import { join as join3, relative } from "node:path";
|
|
747
|
-
var OUTPUT_SUBDIR = "docs/architecture";
|
|
748
|
-
function withoutTsExtension(path) {
|
|
749
|
-
return path.replace(/\.tsx?$/, "");
|
|
750
|
-
}
|
|
751
|
-
function toPosix(path) {
|
|
752
|
-
return path.replace(/\\/g, "/");
|
|
753
|
-
}
|
|
754
|
-
function isSrcIndex(path) {
|
|
755
|
-
return path === "src/index.ts" || path.endsWith("/src/index.ts");
|
|
910
|
+
function isSrcIndex(path) {
|
|
911
|
+
return path === "src/index.ts" || path.endsWith("/src/index.ts");
|
|
756
912
|
}
|
|
757
913
|
function relativePosix(root, path) {
|
|
758
914
|
return toPosix(relative(root, path));
|
|
@@ -761,894 +917,761 @@ function maskRoot(text, root) {
|
|
|
761
917
|
return text.split(root).join("<root>").split(toPosix(root)).join("<root>");
|
|
762
918
|
}
|
|
763
919
|
function outputDirOf(root) {
|
|
764
|
-
return
|
|
920
|
+
return join(root, "docs", "architecture");
|
|
765
921
|
}
|
|
766
922
|
|
|
767
|
-
// src/depgraph/
|
|
768
|
-
var
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
923
|
+
// src/depgraph/duplicates.ts
|
|
924
|
+
var RUNTIME_DUP_CATEGORIES = [
|
|
925
|
+
{ cat: "function", key: "functions" },
|
|
926
|
+
{ cat: "constant", key: "constants" },
|
|
927
|
+
{ cat: "class", key: "classes" }
|
|
928
|
+
];
|
|
929
|
+
var TYPE_DUP_CATEGORIES = [
|
|
930
|
+
{ cat: "interface", key: "interfaces" },
|
|
931
|
+
{ cat: "type", key: "types" },
|
|
932
|
+
{ cat: "enum", key: "enums" }
|
|
933
|
+
];
|
|
934
|
+
var DEFAULT_DISPATCH_CALLS = ["mathTyped"];
|
|
935
|
+
function describeDispatchCalls(calls, code = false) {
|
|
936
|
+
const items = calls.map((call) => code ? `\`${call}(...)\`` : `${call}(...)`);
|
|
937
|
+
if (items.length <= 1)
|
|
938
|
+
return items.join("");
|
|
939
|
+
return `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
|
|
940
|
+
}
|
|
941
|
+
function duplicateSymbolsNote(dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
942
|
+
const dispatch = dispatchCalls.length === 0 ? "DISPATCH_VARIANT (never set: no dispatch call is configured), " : `DISPATCH_VARIANT (>=2 ${describeDispatchCalls(dispatchCalls)} registrations of the ` + "same public name — distinct dispatch surfaces, not copy-paste bodies), ";
|
|
943
|
+
return "This report groups names by OWN definition, not by call graph, then classifies each " + "flagged name (see DupEntryTag): TRUE_DUPLICATE (the actionable merge targets), " + dispatch + "ALIAS_DELEGATION (a const-alias forward to an imported symbol, not an independent body — " + "excluded once fewer than 2 real bodies remain), and ALLOWLISTED (matches " + "duplicate-allowlist.json: for example hot-path is* guards, mirrors in another language, " + "per-package VERSION strings). NOT detected: same-file typed-dispatch overload polymorphism " + "for different argument shapes within one registration — a human still triages " + "TRUE_DUPLICATE entries using the defining files + public flags before merging anything.";
|
|
944
|
+
}
|
|
945
|
+
var DUPLICATE_SYMBOLS_NOTE = duplicateSymbolsNote();
|
|
946
|
+
var DUP_ENTRY_TAG_SORT_ORDER = {
|
|
947
|
+
TRUE_DUPLICATE: 0,
|
|
948
|
+
DISPATCH_VARIANT: 1,
|
|
949
|
+
ALIAS_DELEGATION: 2,
|
|
950
|
+
ALLOWLISTED: 3
|
|
951
|
+
};
|
|
952
|
+
function loadDuplicateAllowlist(path) {
|
|
953
|
+
try {
|
|
954
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
955
|
+
return parsed.entries ?? [];
|
|
956
|
+
} catch {
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
779
959
|
}
|
|
780
|
-
function
|
|
781
|
-
|
|
960
|
+
function globMatchSingle(pattern, value) {
|
|
961
|
+
if (pattern === "*")
|
|
962
|
+
return true;
|
|
963
|
+
if (pattern.endsWith("/**"))
|
|
964
|
+
return value.startsWith(pattern.slice(0, -2));
|
|
965
|
+
if (pattern.endsWith("*"))
|
|
966
|
+
return value.startsWith(pattern.slice(0, -1));
|
|
967
|
+
return value === pattern;
|
|
782
968
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
var CONFIG_FILE = "repo-tools.config.json";
|
|
786
|
-
var DEFAULT_API_ENTRY = "src/index.ts";
|
|
787
|
-
var ABSOLUTE_PATH_TEXT = "holds an absolute path; pass a path relative to the root";
|
|
788
|
-
function isAbsolutePath(path) {
|
|
789
|
-
return posix.isAbsolute(path) || win32.isAbsolute(path);
|
|
969
|
+
function findAllowlistMatch(allowlist, name2, filePath) {
|
|
970
|
+
return allowlist.find((e) => e.names.some((n) => globMatchSingle(n, name2)) && e.filesGlob.some((f) => globMatchSingle(f, filePath)));
|
|
790
971
|
}
|
|
791
|
-
function
|
|
792
|
-
return
|
|
972
|
+
function escapeRegExpLiteral(s) {
|
|
973
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
793
974
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
strictOrphans: "boolean",
|
|
801
|
-
duplicateAllowlist: "path",
|
|
802
|
-
duplicateBaseline: "path",
|
|
803
|
-
coveragePolicy: "path",
|
|
804
|
-
regenerateCommand: "string",
|
|
805
|
-
verificationMarker: "string-or-null",
|
|
806
|
-
extensions: "paths"
|
|
807
|
-
};
|
|
808
|
-
var API_SURFACE_KEYS = {
|
|
809
|
-
out: "path-or-null",
|
|
810
|
-
entry: "path",
|
|
811
|
-
stabilityTags: "names"
|
|
812
|
-
};
|
|
813
|
-
function isObject(value) {
|
|
814
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
975
|
+
function isDispatchVariantBody(code, name2, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
976
|
+
if (dispatchCalls.length === 0)
|
|
977
|
+
return false;
|
|
978
|
+
const calls = dispatchCalls.map(escapeRegExpLiteral).join("|");
|
|
979
|
+
const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*(?:${calls})\\s*(?:<[^>]*>)?\\s*\\(`);
|
|
980
|
+
return re.test(code);
|
|
815
981
|
}
|
|
816
|
-
function
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
};
|
|
833
|
-
switch (kind) {
|
|
834
|
-
case "path":
|
|
835
|
-
return relPath(value, "a non-empty relative path");
|
|
836
|
-
case "path-or-null":
|
|
837
|
-
return value === null ? null : relPath(value, "a non-empty relative path or null");
|
|
838
|
-
case "paths":
|
|
839
|
-
return list(value, (x) => relPath(x, "a list of non-empty relative paths"), "a list of non-empty relative paths");
|
|
840
|
-
case "src":
|
|
841
|
-
if (value === "auto")
|
|
842
|
-
return value;
|
|
843
|
-
return list(value, (x) => relPath(x, '"auto" or a list of non-empty relative paths'), '"auto" or a list of non-empty relative paths');
|
|
844
|
-
case "names":
|
|
845
|
-
return list(value, (x) => text(x, "a list of non-empty strings"), "a list of non-empty strings");
|
|
846
|
-
case "boolean":
|
|
847
|
-
if (typeof value !== "boolean")
|
|
848
|
-
throw fail(`'${key}' must be true or false`);
|
|
849
|
-
return value;
|
|
850
|
-
case "string":
|
|
851
|
-
return text(value, "a non-empty string");
|
|
852
|
-
case "string-or-null":
|
|
853
|
-
return value === null ? null : text(value, "a non-empty string or null");
|
|
854
|
-
case "count":
|
|
855
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
856
|
-
throw fail(`'${key}' must be a whole number of 1 or more`);
|
|
857
|
-
}
|
|
858
|
-
return value;
|
|
859
|
-
case "fraction":
|
|
860
|
-
if (typeof value !== "number" || !(value >= 0 && value <= 1)) {
|
|
861
|
-
throw fail(`'${key}' must be a number from 0 to 1`);
|
|
982
|
+
function collectImportedLocalNames(code) {
|
|
983
|
+
const names = new Set;
|
|
984
|
+
const importRegex = /import\s+(?:type\s+)?(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)))?)\s+from\s+['"][^'"]+['"]/g;
|
|
985
|
+
for (const m of code.matchAll(importRegex)) {
|
|
986
|
+
const named = m[1] || m[4] || "";
|
|
987
|
+
const def = m[2] || m[5] || "";
|
|
988
|
+
const ns = m[3] || "";
|
|
989
|
+
if (named) {
|
|
990
|
+
for (const item of named.split(",")) {
|
|
991
|
+
const trimmed = item.trim().replace(/^type\s+/, "");
|
|
992
|
+
if (!trimmed)
|
|
993
|
+
continue;
|
|
994
|
+
const parts2 = trimmed.split(/\s+as\s+/);
|
|
995
|
+
const local = (parts2[parts2.length - 1] ?? "").trim();
|
|
996
|
+
if (local)
|
|
997
|
+
names.add(local);
|
|
862
998
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
for (const [name2, value] of Object.entries(object)) {
|
|
869
|
-
const kind = keys[name2];
|
|
870
|
-
if (kind === undefined)
|
|
871
|
-
throw fail(`unknown key '${prefix}${name2}'`);
|
|
872
|
-
out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
|
|
999
|
+
}
|
|
1000
|
+
if (def)
|
|
1001
|
+
names.add(def);
|
|
1002
|
+
if (ns)
|
|
1003
|
+
names.add(ns);
|
|
873
1004
|
}
|
|
874
|
-
return
|
|
1005
|
+
return names;
|
|
875
1006
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
var CLONES_KEYS = {
|
|
881
|
-
src: "src",
|
|
882
|
-
out: "path-or-null",
|
|
883
|
-
minNodes: "count",
|
|
884
|
-
shingleLength: "count",
|
|
885
|
-
nearThreshold: "fraction",
|
|
886
|
-
containmentThreshold: "fraction",
|
|
887
|
-
containmentMinShingles: "count",
|
|
888
|
-
rareShingleLimit: "count",
|
|
889
|
-
minSharedShingles: "count"
|
|
890
|
-
};
|
|
891
|
-
function checkClonesFlag(label, key, value) {
|
|
892
|
-
return checkValue(label, CLONES_KEYS[key], value, (message) => new Error(message));
|
|
1007
|
+
function isAliasDelegationBody(code, name2, importedLocalNames) {
|
|
1008
|
+
const re = new RegExp(`export\\s+const\\s+${escapeRegExpLiteral(name2)}\\s*(?::[^=]+)?=\\s*([A-Za-z_$][\\w$]*)\\s*;`);
|
|
1009
|
+
const m = code.match(re);
|
|
1010
|
+
return !!m && importedLocalNames.has(m[1] ?? "");
|
|
893
1011
|
}
|
|
894
|
-
function
|
|
895
|
-
const
|
|
896
|
-
if (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1012
|
+
function classifyDefiner(file, name2, category, allowlist, readRaw, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
1013
|
+
const allowMatch = findAllowlistMatch(allowlist, name2, file.path);
|
|
1014
|
+
if (allowMatch)
|
|
1015
|
+
return { tag: "ALLOWLISTED", reason: allowMatch.reason };
|
|
1016
|
+
if (category === "constant") {
|
|
1017
|
+
const raw = readRaw(file.path);
|
|
1018
|
+
if (raw) {
|
|
1019
|
+
const code = stripComments(raw);
|
|
1020
|
+
if (isDispatchVariantBody(code, name2, dispatchCalls))
|
|
1021
|
+
return { tag: "DISPATCH_VARIANT" };
|
|
1022
|
+
if (isAliasDelegationBody(code, name2, collectImportedLocalNames(code))) {
|
|
1023
|
+
return { tag: "ALIAS_DELEGATION" };
|
|
1024
|
+
}
|
|
903
1025
|
}
|
|
904
1026
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1027
|
+
return { tag: "PLAIN" };
|
|
1028
|
+
}
|
|
1029
|
+
function collectOwnDefiners(files, categories) {
|
|
1030
|
+
const byName = new Map;
|
|
1031
|
+
for (const file of files) {
|
|
1032
|
+
const reExported = new Set(file.exports.reExported);
|
|
1033
|
+
for (const { cat, key } of categories) {
|
|
1034
|
+
for (const name2 of file.exports[key]) {
|
|
1035
|
+
if (reExported.has(name2))
|
|
1036
|
+
continue;
|
|
1037
|
+
if (key === "types" && file.exports.interfaces.includes(name2))
|
|
1038
|
+
continue;
|
|
1039
|
+
const list = byName.get(name2) ?? [];
|
|
1040
|
+
list.push({ file, category: cat });
|
|
1041
|
+
byName.set(name2, list);
|
|
1042
|
+
}
|
|
920
1043
|
}
|
|
921
1044
|
}
|
|
922
|
-
|
|
923
|
-
if (query !== undefined) {
|
|
924
|
-
if (!isObject(query))
|
|
925
|
-
throw fail("'query' must be an object");
|
|
926
|
-
sections.query = checkSection("query.", query, QUERY_KEYS, fail);
|
|
927
|
-
}
|
|
928
|
-
const clones = parsed.clones;
|
|
929
|
-
if (clones !== undefined) {
|
|
930
|
-
if (!isObject(clones))
|
|
931
|
-
throw fail("'clones' must be an object");
|
|
932
|
-
sections.clones = checkSection("clones.", clones, CLONES_KEYS, fail);
|
|
933
|
-
}
|
|
934
|
-
return sections;
|
|
1045
|
+
return byName;
|
|
935
1046
|
}
|
|
936
|
-
function
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1047
|
+
function finalizeDuplicateEntry(name2, categories, definers, tag) {
|
|
1048
|
+
const publicDefiners = definers.filter((d) => d.public);
|
|
1049
|
+
const canonicalHint = publicDefiners.length === 1 ? publicDefiners[0]?.file ?? "" : publicDefiners.length > 1 ? "AMBIGUOUS" : "internal-only";
|
|
1050
|
+
return { name: name2, category: [...categories].sort().join("+"), definers, canonicalHint, tag };
|
|
1051
|
+
}
|
|
1052
|
+
function buildDuplicateEntries(byName, publicSurface, allowlist, readRaw, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
1053
|
+
const isFilePublic = (file, name2) => publicSurface.publicWildcardFiles.has(file.path) || publicSurface.publicNamed.has(`${file.path}::${name2}`);
|
|
1054
|
+
const entries = [];
|
|
1055
|
+
for (const [name2, defs] of byName) {
|
|
1056
|
+
const byFile = new Map;
|
|
1057
|
+
for (const d of defs)
|
|
1058
|
+
if (!byFile.has(d.file.path))
|
|
1059
|
+
byFile.set(d.file.path, d);
|
|
1060
|
+
if (byFile.size < 2)
|
|
1061
|
+
continue;
|
|
1062
|
+
const categories = new Set;
|
|
1063
|
+
const definers = [];
|
|
1064
|
+
for (const { file, category } of byFile.values()) {
|
|
1065
|
+
categories.add(category);
|
|
1066
|
+
const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw, dispatchCalls);
|
|
1067
|
+
definers.push({
|
|
1068
|
+
file: file.path,
|
|
1069
|
+
package: file.packageName ?? "unknown",
|
|
1070
|
+
public: isFilePublic(file, name2),
|
|
1071
|
+
tag,
|
|
1072
|
+
...reason ? { reason } : {}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
definers.sort((a, b) => compareCodeUnits(a.file, b.file));
|
|
1076
|
+
const nonAlias = definers.filter((d) => d.tag !== "ALIAS_DELEGATION");
|
|
1077
|
+
if (nonAlias.length < 2) {
|
|
1078
|
+
entries.push(finalizeDuplicateEntry(name2, categories, definers, "ALIAS_DELEGATION"));
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const nonAllowlisted = nonAlias.filter((d) => d.tag !== "ALLOWLISTED");
|
|
1082
|
+
let entryTag;
|
|
1083
|
+
if (nonAllowlisted.length < 2) {
|
|
1084
|
+
entryTag = "ALLOWLISTED";
|
|
1085
|
+
} else {
|
|
1086
|
+
const dispatchCount = nonAllowlisted.filter((d) => d.tag === "DISPATCH_VARIANT").length;
|
|
1087
|
+
entryTag = dispatchCount >= 2 ? "DISPATCH_VARIANT" : "TRUE_DUPLICATE";
|
|
1088
|
+
}
|
|
1089
|
+
entries.push(finalizeDuplicateEntry(name2, categories, definers, entryTag));
|
|
956
1090
|
}
|
|
957
|
-
|
|
1091
|
+
entries.sort((a, b) => DUP_ENTRY_TAG_SORT_ORDER[a.tag] - DUP_ENTRY_TAG_SORT_ORDER[b.tag] || b.definers.length - a.definers.length || compareCodeUnits(a.name, b.name));
|
|
1092
|
+
return entries;
|
|
958
1093
|
}
|
|
959
|
-
function
|
|
960
|
-
|
|
1094
|
+
function tallyByTag(entries) {
|
|
1095
|
+
const tally = {
|
|
1096
|
+
TRUE_DUPLICATE: 0,
|
|
1097
|
+
DISPATCH_VARIANT: 0,
|
|
1098
|
+
ALIAS_DELEGATION: 0,
|
|
1099
|
+
ALLOWLISTED: 0
|
|
1100
|
+
};
|
|
1101
|
+
for (const e of entries)
|
|
1102
|
+
tally[e.tag]++;
|
|
1103
|
+
return tally;
|
|
961
1104
|
}
|
|
962
|
-
function
|
|
1105
|
+
function detectDuplicateSymbols(files, publicSurface, root, allowlistPath = join2(outputDirOf(root), "duplicate-allowlist.json"), dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
1106
|
+
const allowlist = loadDuplicateAllowlist(allowlistPath);
|
|
1107
|
+
const cache = new Map;
|
|
1108
|
+
const readRaw = (relPath) => {
|
|
1109
|
+
let content = cache.get(relPath);
|
|
1110
|
+
if (content === undefined) {
|
|
1111
|
+
try {
|
|
1112
|
+
content = readFileSync(join2(root, relPath), "utf-8");
|
|
1113
|
+
} catch {
|
|
1114
|
+
content = "";
|
|
1115
|
+
}
|
|
1116
|
+
cache.set(relPath, content);
|
|
1117
|
+
}
|
|
1118
|
+
return content;
|
|
1119
|
+
};
|
|
963
1120
|
return {
|
|
964
|
-
|
|
965
|
-
|
|
1121
|
+
runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls),
|
|
1122
|
+
types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls)
|
|
966
1123
|
};
|
|
967
1124
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
const inDocs = (name2) => `${OUTPUT_SUBDIR}/${name2}`;
|
|
1125
|
+
function buildDuplicateReport(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
1126
|
+
const runtimeByTag = tallyByTag(dup.runtime);
|
|
1127
|
+
const typeByTag = tallyByTag(dup.types);
|
|
972
1128
|
return {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1129
|
+
note: duplicateSymbolsNote(dispatchCalls),
|
|
1130
|
+
summary: {
|
|
1131
|
+
runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
|
|
1132
|
+
typeDuplicates: typeByTag.TRUE_DUPLICATE,
|
|
1133
|
+
runtimeByTag,
|
|
1134
|
+
typeByTag
|
|
1135
|
+
},
|
|
1136
|
+
runtime: dup.runtime,
|
|
1137
|
+
types: dup.types
|
|
978
1138
|
};
|
|
979
1139
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const
|
|
1140
|
+
var DUPLICATE_BASELINE_NOTE = "The accepted TRUE_DUPLICATE names of each kind, with their defining files. " + "`repo-tools depgraph --check-duplicates` fails only on a TRUE_DUPLICATE name that this file " + "does not hold. After a reviewed change, write it again with " + "`repo-tools depgraph --write-duplicate-baseline`.";
|
|
1141
|
+
function trueDuplicateNames(entries) {
|
|
1142
|
+
const names = {};
|
|
1143
|
+
const found = entries.filter((e) => e.tag === "TRUE_DUPLICATE").sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
1144
|
+
for (const e of found)
|
|
1145
|
+
names[e.name] = e.definers.map((d) => d.file).sort(compareCodeUnits);
|
|
1146
|
+
return names;
|
|
1147
|
+
}
|
|
1148
|
+
function buildDuplicateBaseline(report) {
|
|
983
1149
|
return {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
exclude: cli.exclude ?? file.exclude ?? [...DEFAULT_EXCLUDE],
|
|
988
|
-
alsoExclude: cli.alsoExclude ?? file.alsoExclude ?? [],
|
|
989
|
-
strictOrphans: cli.strictOrphans ?? file.strictOrphans ?? false,
|
|
990
|
-
duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
|
|
991
|
-
duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
|
|
992
|
-
coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
|
|
993
|
-
regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
|
|
994
|
-
verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
|
|
995
|
-
apiSurface: {
|
|
996
|
-
out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
|
|
997
|
-
entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
|
|
998
|
-
stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
|
|
999
|
-
},
|
|
1000
|
-
extensions: cli.extensions ?? file.extensions ?? []
|
|
1150
|
+
note: DUPLICATE_BASELINE_NOTE,
|
|
1151
|
+
runtime: trueDuplicateNames(report.runtime),
|
|
1152
|
+
types: trueDuplicateNames(report.types)
|
|
1001
1153
|
};
|
|
1002
1154
|
}
|
|
1155
|
+
function findNewDuplicates(report, baseline) {
|
|
1156
|
+
const found = [];
|
|
1157
|
+
for (const kind of ["runtime", "types"]) {
|
|
1158
|
+
const held = baseline[kind];
|
|
1159
|
+
for (const [name2, files] of Object.entries(trueDuplicateNames(report[kind]))) {
|
|
1160
|
+
if (!Object.hasOwn(held, name2))
|
|
1161
|
+
found.push({ kind, name: name2, files });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return found;
|
|
1165
|
+
}
|
|
1166
|
+
function isRecord(value) {
|
|
1167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1168
|
+
}
|
|
1169
|
+
function asDuplicateEntries(parsed) {
|
|
1170
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const valid = (e) => isRecord(e) && typeof e.name === "string" && typeof e.tag === "string" && Array.isArray(e.definers) && e.definers.every((d) => isRecord(d) && typeof d.file === "string");
|
|
1174
|
+
if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
|
|
1175
|
+
return null;
|
|
1176
|
+
return parsed;
|
|
1177
|
+
}
|
|
1178
|
+
function asBaselineNames(parsed) {
|
|
1179
|
+
if (!isRecord(parsed))
|
|
1180
|
+
return null;
|
|
1181
|
+
const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
|
|
1182
|
+
const runtime = kind(parsed.runtime);
|
|
1183
|
+
const types = kind(parsed.types);
|
|
1184
|
+
return runtime && types ? { runtime, types } : null;
|
|
1185
|
+
}
|
|
1003
1186
|
|
|
1004
|
-
// src/depgraph/
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
"-t": "the test coverage reports are always written"
|
|
1187
|
+
// src/depgraph/exclude.ts
|
|
1188
|
+
import { statSync } from "node:fs";
|
|
1189
|
+
import { join as join4 } from "node:path";
|
|
1190
|
+
|
|
1191
|
+
// src/depgraph/dirlist.ts
|
|
1192
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
1193
|
+
import { join as join3 } from "node:path";
|
|
1194
|
+
var readdirHook = {
|
|
1195
|
+
names: (dir) => readdirSync(dir),
|
|
1196
|
+
entries: (dir) => readdirSync(dir, { withFileTypes: true })
|
|
1015
1197
|
};
|
|
1016
|
-
function
|
|
1017
|
-
return
|
|
1198
|
+
function listNames(dir) {
|
|
1199
|
+
return sortCodeUnits(readdirHook.names(dir));
|
|
1018
1200
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
"-a": "all",
|
|
1022
|
-
"--include-tests": "includeTests",
|
|
1023
|
-
"-t": "includeTests",
|
|
1024
|
-
"--reachable-only": "reachableOnly",
|
|
1025
|
-
"--strict-orphans": "strictOrphans",
|
|
1026
|
-
"--strict-census": "strictCensus",
|
|
1027
|
-
"--check-census": "checkCensus",
|
|
1028
|
-
"--check-duplicates": "checkDuplicates",
|
|
1029
|
-
"--no-regen": "noRegen",
|
|
1030
|
-
"--write-duplicate-baseline": "writeDuplicateBaseline",
|
|
1031
|
-
"--no-extensions": "noExtensions",
|
|
1032
|
-
"--help": "help",
|
|
1033
|
-
"-h": "help"
|
|
1034
|
-
};
|
|
1035
|
-
var VALUE_FLAGS = {
|
|
1036
|
-
"--root": (_o, value, setRoot) => setRoot(value),
|
|
1037
|
-
"--config": (o, value) => {
|
|
1038
|
-
o.config = relativePath("--config", value);
|
|
1039
|
-
},
|
|
1040
|
-
"--src": (o, value) => {
|
|
1041
|
-
o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
|
|
1042
|
-
},
|
|
1043
|
-
"--tests": (o, value) => {
|
|
1044
|
-
o.settings.tests = pathList("--tests", value);
|
|
1045
|
-
},
|
|
1046
|
-
"--out": (o, value) => {
|
|
1047
|
-
o.settings.out = relativePath("--out", value);
|
|
1048
|
-
},
|
|
1049
|
-
"--exclude": (o, value) => {
|
|
1050
|
-
o.settings.exclude = list("--exclude", value);
|
|
1051
|
-
},
|
|
1052
|
-
"--also-exclude": (o, value) => {
|
|
1053
|
-
o.settings.alsoExclude = list("--also-exclude", value);
|
|
1054
|
-
},
|
|
1055
|
-
"--api-surface": (o, value) => {
|
|
1056
|
-
o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
|
|
1057
|
-
},
|
|
1058
|
-
"--api-entry": (o, value) => {
|
|
1059
|
-
o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
|
|
1060
|
-
},
|
|
1061
|
-
"--stability-tags": (o, value) => {
|
|
1062
|
-
o.settings.apiSurface = {
|
|
1063
|
-
...o.settings.apiSurface,
|
|
1064
|
-
stabilityTags: list("--stability-tags", value)
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
function list(flag, value) {
|
|
1069
|
-
const items = value.split(",");
|
|
1070
|
-
if (items.some((item) => item === ""))
|
|
1071
|
-
throw new Error(`flag ${flag} holds an empty item`);
|
|
1072
|
-
return items;
|
|
1073
|
-
}
|
|
1074
|
-
function pathList(flag, value) {
|
|
1075
|
-
return list(flag, value).map((item) => relativePath(flag, item));
|
|
1076
|
-
}
|
|
1077
|
-
function relativePath(flag, value) {
|
|
1078
|
-
if (isAbsolutePath(value)) {
|
|
1079
|
-
throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
|
|
1080
|
-
}
|
|
1081
|
-
return value;
|
|
1082
|
-
}
|
|
1083
|
-
function parseDepgraphArgs(argv, cwd, command) {
|
|
1084
|
-
const options = {
|
|
1085
|
-
root: cwd,
|
|
1086
|
-
settings: {},
|
|
1087
|
-
includeTests: false,
|
|
1088
|
-
all: false,
|
|
1089
|
-
reachableOnly: false,
|
|
1090
|
-
strictOrphans: false,
|
|
1091
|
-
strictCensus: false,
|
|
1092
|
-
checkCensus: false,
|
|
1093
|
-
checkDuplicates: false,
|
|
1094
|
-
noRegen: false,
|
|
1095
|
-
writeDuplicateBaseline: false,
|
|
1096
|
-
noExtensions: false,
|
|
1097
|
-
help: false,
|
|
1098
|
-
ignoredFlags: []
|
|
1099
|
-
};
|
|
1100
|
-
let rootSet = false;
|
|
1101
|
-
const setRoot = (value) => {
|
|
1102
|
-
if (rootSet)
|
|
1103
|
-
throw new Error("the root is set twice (use --root=<path> or one positional path)");
|
|
1104
|
-
rootSet = true;
|
|
1105
|
-
options.root = value;
|
|
1106
|
-
};
|
|
1107
|
-
for (const arg of argv) {
|
|
1108
|
-
if (!arg.startsWith("-")) {
|
|
1109
|
-
setRoot(arg);
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
const eq = arg.indexOf("=");
|
|
1113
|
-
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
1114
|
-
const value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
1115
|
-
if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
|
|
1116
|
-
if (command === "map")
|
|
1117
|
-
throw new Error(scanScopeText(name2));
|
|
1118
|
-
const takesValue = name2 in VALUE_FLAGS;
|
|
1119
|
-
if (!takesValue && value !== undefined)
|
|
1120
|
-
throw new Error(`flag ${name2} takes no value`);
|
|
1121
|
-
if (takesValue && (value === undefined || value === "")) {
|
|
1122
|
-
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1123
|
-
}
|
|
1124
|
-
options.ignoredFlags.push(name2);
|
|
1125
|
-
continue;
|
|
1126
|
-
}
|
|
1127
|
-
const booleanKey = BOOLEAN_FLAGS[name2];
|
|
1128
|
-
if (booleanKey !== undefined) {
|
|
1129
|
-
if (value !== undefined)
|
|
1130
|
-
throw new Error(`flag ${name2} takes no value`);
|
|
1131
|
-
options[booleanKey] = true;
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
const apply = VALUE_FLAGS[name2];
|
|
1135
|
-
if (apply !== undefined) {
|
|
1136
|
-
if (value === undefined || value === "") {
|
|
1137
|
-
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1138
|
-
}
|
|
1139
|
-
apply(options, value, setRoot);
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
|
|
1143
|
-
}
|
|
1144
|
-
checkModes(options);
|
|
1145
|
-
return options;
|
|
1146
|
-
}
|
|
1147
|
-
function checkModes(options) {
|
|
1148
|
-
if (options.noRegen && !options.checkDuplicates) {
|
|
1149
|
-
throw new Error("flag --no-regen applies with --check-duplicates only");
|
|
1150
|
-
}
|
|
1151
|
-
const modes = [
|
|
1152
|
-
options.checkCensus && "--check-census",
|
|
1153
|
-
options.checkDuplicates && "--check-duplicates",
|
|
1154
|
-
options.writeDuplicateBaseline && "--write-duplicate-baseline"
|
|
1155
|
-
].filter((mode) => mode !== false);
|
|
1156
|
-
if (modes.length > 1) {
|
|
1157
|
-
throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
function wantsHelp(argv) {
|
|
1161
|
-
return argv.includes("--help") || argv.includes("-h");
|
|
1201
|
+
function listEntries(dir) {
|
|
1202
|
+
return [...readdirHook.entries(dir)].sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
1162
1203
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1170
|
-
import { join as join4 } from "node:path";
|
|
1171
|
-
|
|
1172
|
-
// src/mask.ts
|
|
1173
|
-
var REGEX_KEYWORDS2 = new Set([
|
|
1174
|
-
"return",
|
|
1175
|
-
"typeof",
|
|
1176
|
-
"instanceof",
|
|
1177
|
-
"in",
|
|
1178
|
-
"of",
|
|
1179
|
-
"new",
|
|
1180
|
-
"delete",
|
|
1181
|
-
"void",
|
|
1182
|
-
"throw",
|
|
1183
|
-
"case",
|
|
1184
|
-
"do",
|
|
1185
|
-
"else",
|
|
1186
|
-
"yield",
|
|
1187
|
-
"await"
|
|
1188
|
-
]);
|
|
1189
|
-
function regexCanStart(src, prev, afterStmtParen) {
|
|
1190
|
-
if (prev < 0)
|
|
1191
|
-
return true;
|
|
1192
|
-
const c = src[prev] ?? "";
|
|
1193
|
-
if (c === ")")
|
|
1194
|
-
return afterStmtParen;
|
|
1195
|
-
if (c === ">")
|
|
1196
|
-
return src[prev - 1] === "=";
|
|
1197
|
-
if ((c === "+" || c === "-") && src[prev - 1] === c)
|
|
1198
|
-
return false;
|
|
1199
|
-
if (/[([{},;:?=!&|+\-*%^~]/.test(c))
|
|
1200
|
-
return true;
|
|
1201
|
-
if (!/[\w$]/.test(c))
|
|
1204
|
+
var linkLog = { skipped: new Set };
|
|
1205
|
+
function isLink(path) {
|
|
1206
|
+
let link;
|
|
1207
|
+
try {
|
|
1208
|
+
link = lstatSync(path).isSymbolicLink();
|
|
1209
|
+
} catch {
|
|
1202
1210
|
return false;
|
|
1203
|
-
const word = /[\w$]+$/.exec(src.slice(Math.max(0, prev - 15), prev + 1))?.[0] ?? "";
|
|
1204
|
-
return REGEX_KEYWORDS2.has(word) && !/[\w$.]/.test(src[prev - word.length] ?? "");
|
|
1205
|
-
}
|
|
1206
|
-
function regexBodyEnd(src, start2) {
|
|
1207
|
-
let inClass = false;
|
|
1208
|
-
for (let j = start2 + 1;j < src.length; j++) {
|
|
1209
|
-
const c = src[j];
|
|
1210
|
-
if (c === `
|
|
1211
|
-
` || c === "\r")
|
|
1212
|
-
return -1;
|
|
1213
|
-
if (c === "\\")
|
|
1214
|
-
j += 1;
|
|
1215
|
-
else if (c === "[")
|
|
1216
|
-
inClass = true;
|
|
1217
|
-
else if (c === "]")
|
|
1218
|
-
inClass = false;
|
|
1219
|
-
else if (c === "/" && !inClass)
|
|
1220
|
-
return j;
|
|
1221
|
-
}
|
|
1222
|
-
return -1;
|
|
1223
|
-
}
|
|
1224
|
-
function scan(src) {
|
|
1225
|
-
const out2 = [];
|
|
1226
|
-
const push = (kind, start2, end) => {
|
|
1227
|
-
if (end <= start2)
|
|
1228
|
-
return;
|
|
1229
|
-
const last = out2[out2.length - 1];
|
|
1230
|
-
if (last && last.kind === kind && last.end === start2)
|
|
1231
|
-
last.end = end;
|
|
1232
|
-
else
|
|
1233
|
-
out2.push({ kind, start: start2, end });
|
|
1234
|
-
};
|
|
1235
|
-
const templates = [];
|
|
1236
|
-
let i2 = 0;
|
|
1237
|
-
const n = src.length;
|
|
1238
|
-
const readTemplateBody = () => {
|
|
1239
|
-
const start2 = i2;
|
|
1240
|
-
while (i2 < n) {
|
|
1241
|
-
const c = src[i2];
|
|
1242
|
-
if (c === "\\") {
|
|
1243
|
-
i2 += 2;
|
|
1244
|
-
continue;
|
|
1245
|
-
}
|
|
1246
|
-
if (c === "`") {
|
|
1247
|
-
push("string", start2, i2);
|
|
1248
|
-
push("code", i2, i2 + 1);
|
|
1249
|
-
i2 += 1;
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
if (c === "$" && src[i2 + 1] === "{") {
|
|
1253
|
-
push("string", start2, i2);
|
|
1254
|
-
push("code", i2, i2 + 2);
|
|
1255
|
-
i2 += 2;
|
|
1256
|
-
templates.push(0);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
i2 += 1;
|
|
1260
|
-
}
|
|
1261
|
-
push("string", start2, n);
|
|
1262
|
-
};
|
|
1263
|
-
let codeStart = 0;
|
|
1264
|
-
let prev = -1;
|
|
1265
|
-
const parenStmt = [];
|
|
1266
|
-
let closedStmt = false;
|
|
1267
|
-
const wordEndingAt = (index) => {
|
|
1268
|
-
let end = index;
|
|
1269
|
-
while (end > 0) {
|
|
1270
|
-
const prev = src[end - 1] ?? "";
|
|
1271
|
-
if (/\s/.test(prev)) {
|
|
1272
|
-
end -= 1;
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
if (prev === "/" && src[end - 2] === "*") {
|
|
1276
|
-
const open = src.lastIndexOf("/*", end - 2);
|
|
1277
|
-
if (open === -1)
|
|
1278
|
-
break;
|
|
1279
|
-
end = open;
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
break;
|
|
1283
|
-
}
|
|
1284
|
-
return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
|
|
1285
|
-
};
|
|
1286
|
-
while (i2 < n) {
|
|
1287
|
-
const c = src[i2];
|
|
1288
|
-
const next = src[i2 + 1];
|
|
1289
|
-
if (c === "/" && next !== "/" && next !== "*" && regexCanStart(src, prev, closedStmt)) {
|
|
1290
|
-
const close = regexBodyEnd(src, i2);
|
|
1291
|
-
if (close !== -1) {
|
|
1292
|
-
push("code", codeStart, i2 + 1);
|
|
1293
|
-
push("string", i2 + 1, close);
|
|
1294
|
-
i2 = close + 1;
|
|
1295
|
-
while (i2 < n && /[a-z]/i.test(src[i2] ?? ""))
|
|
1296
|
-
i2 += 1;
|
|
1297
|
-
push("code", close, i2);
|
|
1298
|
-
prev = i2 - 1;
|
|
1299
|
-
closedStmt = false;
|
|
1300
|
-
codeStart = i2;
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
if (c === "/" && next === "/") {
|
|
1305
|
-
push("code", codeStart, i2);
|
|
1306
|
-
const eol = src.indexOf(`
|
|
1307
|
-
`, i2);
|
|
1308
|
-
const end = eol === -1 ? n : eol;
|
|
1309
|
-
push("comment", i2, end);
|
|
1310
|
-
i2 = end;
|
|
1311
|
-
codeStart = i2;
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
if (c === "/" && next === "*") {
|
|
1315
|
-
push("code", codeStart, i2);
|
|
1316
|
-
const close = src.indexOf("*/", i2 + 2);
|
|
1317
|
-
const end = close === -1 ? n : close + 2;
|
|
1318
|
-
push("comment", i2, end);
|
|
1319
|
-
i2 = end;
|
|
1320
|
-
codeStart = i2;
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1323
|
-
if (c === "'" || c === '"') {
|
|
1324
|
-
push("code", codeStart, i2 + 1);
|
|
1325
|
-
let j = i2 + 1;
|
|
1326
|
-
while (j < n && src[j] !== c && src[j] !== `
|
|
1327
|
-
`)
|
|
1328
|
-
j += src[j] === "\\" ? 2 : 1;
|
|
1329
|
-
const end = Math.min(j, n);
|
|
1330
|
-
push("string", i2 + 1, end);
|
|
1331
|
-
i2 = end < n && src[end] === c ? end + 1 : end;
|
|
1332
|
-
push("code", end, i2);
|
|
1333
|
-
prev = i2 - 1;
|
|
1334
|
-
closedStmt = false;
|
|
1335
|
-
codeStart = i2;
|
|
1336
|
-
continue;
|
|
1337
|
-
}
|
|
1338
|
-
if (c === "`") {
|
|
1339
|
-
push("code", codeStart, i2 + 1);
|
|
1340
|
-
i2 += 1;
|
|
1341
|
-
readTemplateBody();
|
|
1342
|
-
prev = i2 - 1;
|
|
1343
|
-
closedStmt = false;
|
|
1344
|
-
codeStart = i2;
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
if (!/\s/.test(c ?? "")) {
|
|
1348
|
-
if (c === "(") {
|
|
1349
|
-
const word = wordEndingAt(i2);
|
|
1350
|
-
parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
|
|
1351
|
-
closedStmt = false;
|
|
1352
|
-
} else if (c === ")")
|
|
1353
|
-
closedStmt = parenStmt.pop() === true;
|
|
1354
|
-
else
|
|
1355
|
-
closedStmt = false;
|
|
1356
|
-
prev = i2;
|
|
1357
|
-
}
|
|
1358
|
-
if (templates.length > 0) {
|
|
1359
|
-
const depth = templates.length - 1;
|
|
1360
|
-
if (c === "{")
|
|
1361
|
-
templates[depth] = (templates[depth] ?? 0) + 1;
|
|
1362
|
-
else if (c === "}") {
|
|
1363
|
-
if (templates[depth] === 0) {
|
|
1364
|
-
templates.pop();
|
|
1365
|
-
push("code", codeStart, i2 + 1);
|
|
1366
|
-
i2 += 1;
|
|
1367
|
-
readTemplateBody();
|
|
1368
|
-
prev = i2 - 1;
|
|
1369
|
-
closedStmt = false;
|
|
1370
|
-
codeStart = i2;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
templates[depth] = (templates[depth] ?? 1) - 1;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
i2 += 1;
|
|
1377
1211
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
function blank(text) {
|
|
1382
|
-
return text.replace(/[^\r\n]/g, " ");
|
|
1212
|
+
if (link)
|
|
1213
|
+
linkLog.skipped.add(path);
|
|
1214
|
+
return link;
|
|
1383
1215
|
}
|
|
1384
|
-
function
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1216
|
+
function isLinkEntry(dir, entry) {
|
|
1217
|
+
if (!entry.isSymbolicLink())
|
|
1218
|
+
return false;
|
|
1219
|
+
linkLog.skipped.add(join3(dir, entry.name));
|
|
1220
|
+
return true;
|
|
1389
1221
|
}
|
|
1390
|
-
function
|
|
1391
|
-
return
|
|
1222
|
+
function isWalkable(dir) {
|
|
1223
|
+
return existsSync(dir) && !isLink(dir);
|
|
1392
1224
|
}
|
|
1393
1225
|
|
|
1394
|
-
// src/depgraph/
|
|
1395
|
-
var
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
{ cat: "interface", key: "interfaces" },
|
|
1402
|
-
{ cat: "type", key: "types" },
|
|
1403
|
-
{ cat: "enum", key: "enums" }
|
|
1226
|
+
// src/depgraph/exclude.ts
|
|
1227
|
+
var DEFAULT_EXCLUDE = [
|
|
1228
|
+
"node_modules",
|
|
1229
|
+
"dist",
|
|
1230
|
+
"build",
|
|
1231
|
+
"coverage",
|
|
1232
|
+
".git"
|
|
1404
1233
|
];
|
|
1405
|
-
var
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
DISPATCH_VARIANT: 1,
|
|
1409
|
-
ALIAS_DELEGATION: 2,
|
|
1410
|
-
ALLOWLISTED: 3
|
|
1411
|
-
};
|
|
1412
|
-
function loadDuplicateAllowlist(path) {
|
|
1413
|
-
try {
|
|
1414
|
-
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1415
|
-
return parsed.entries ?? [];
|
|
1416
|
-
} catch {
|
|
1417
|
-
return [];
|
|
1418
|
-
}
|
|
1234
|
+
var TEST_DIR_NAMES = ["test", "tests"];
|
|
1235
|
+
function isSourceInput(name2) {
|
|
1236
|
+
return (name2.endsWith(".ts") || name2.endsWith(".tsx")) && !name2.endsWith(".d.ts") && !/\.(test|spec)\.tsx?$/.test(name2);
|
|
1419
1237
|
}
|
|
1420
|
-
function
|
|
1421
|
-
if (
|
|
1422
|
-
return
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1238
|
+
function sourceTsFilesUnder(dir, skip = new Set(DEFAULT_EXCLUDE), files = []) {
|
|
1239
|
+
if (!isWalkable(dir))
|
|
1240
|
+
return files;
|
|
1241
|
+
for (const entry of listNames(dir)) {
|
|
1242
|
+
if (skip.has(entry))
|
|
1243
|
+
continue;
|
|
1244
|
+
const fullPath = join4(dir, entry);
|
|
1245
|
+
if (isLink(fullPath))
|
|
1246
|
+
continue;
|
|
1247
|
+
if (statSync(fullPath).isDirectory())
|
|
1248
|
+
sourceTsFilesUnder(fullPath, skip, files);
|
|
1249
|
+
else if (isSourceInput(entry))
|
|
1250
|
+
files.push(fullPath);
|
|
1251
|
+
}
|
|
1252
|
+
return files;
|
|
1428
1253
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1254
|
+
|
|
1255
|
+
// src/depgraph/reporters/banner.ts
|
|
1256
|
+
var VERIFICATION_MARKER = "<!-- repo-map:no-verification -->";
|
|
1257
|
+
var DEFAULT_REGENERATE_COMMAND = "repo-tools depgraph";
|
|
1258
|
+
function bannerFor(options) {
|
|
1259
|
+
const command = options.command ?? DEFAULT_REGENERATE_COMMAND;
|
|
1260
|
+
const marker = options.marker === undefined ? VERIFICATION_MARKER : options.marker;
|
|
1261
|
+
const comment = `<!-- GENERATED FILE -- do not edit by hand.
|
|
1262
|
+
Regenerate with \`${command}\`. -->
|
|
1263
|
+
|
|
1264
|
+
`;
|
|
1265
|
+
return marker === null ? comment : `${marker}
|
|
1266
|
+
${comment}`;
|
|
1431
1267
|
}
|
|
1432
|
-
function
|
|
1433
|
-
return
|
|
1268
|
+
function withBanner(body2, options = {}) {
|
|
1269
|
+
return bannerFor(options) + body2;
|
|
1434
1270
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1271
|
+
|
|
1272
|
+
// src/config.ts
|
|
1273
|
+
var CONFIG_FILE = "repo-tools.config.json";
|
|
1274
|
+
var DEFAULT_API_ENTRY = "src/index.ts";
|
|
1275
|
+
var ABSOLUTE_PATH_TEXT = "holds an absolute path; pass a path relative to the root";
|
|
1276
|
+
function isAbsolutePath(path) {
|
|
1277
|
+
return posix.isAbsolute(path) || win32.isAbsolute(path);
|
|
1438
1278
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
const importRegex = /import\s+(?:type\s+)?(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)))?)\s+from\s+['"][^'"]+['"]/g;
|
|
1442
|
-
for (const m of code.matchAll(importRegex)) {
|
|
1443
|
-
const named = m[1] || m[4] || "";
|
|
1444
|
-
const def = m[2] || m[5] || "";
|
|
1445
|
-
const ns = m[3] || "";
|
|
1446
|
-
if (named) {
|
|
1447
|
-
for (const item of named.split(",")) {
|
|
1448
|
-
const trimmed = item.trim().replace(/^type\s+/, "");
|
|
1449
|
-
if (!trimmed)
|
|
1450
|
-
continue;
|
|
1451
|
-
const parts2 = trimmed.split(/\s+as\s+/);
|
|
1452
|
-
const local = (parts2[parts2.length - 1] ?? "").trim();
|
|
1453
|
-
if (local)
|
|
1454
|
-
names.add(local);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (def)
|
|
1458
|
-
names.add(def);
|
|
1459
|
-
if (ns)
|
|
1460
|
-
names.add(ns);
|
|
1461
|
-
}
|
|
1462
|
-
return names;
|
|
1279
|
+
function resolveUnderRoot(root, path) {
|
|
1280
|
+
return resolve(root, path);
|
|
1463
1281
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1282
|
+
var DEPGRAPH_KEYS = {
|
|
1283
|
+
src: "src",
|
|
1284
|
+
tests: "paths",
|
|
1285
|
+
out: "path",
|
|
1286
|
+
exclude: "names",
|
|
1287
|
+
alsoExclude: "names",
|
|
1288
|
+
strictOrphans: "boolean",
|
|
1289
|
+
dispatchCalls: "calls",
|
|
1290
|
+
duplicateAllowlist: "path",
|
|
1291
|
+
duplicateBaseline: "path",
|
|
1292
|
+
coveragePolicy: "path",
|
|
1293
|
+
regenerateCommand: "string",
|
|
1294
|
+
verificationMarker: "string-or-null",
|
|
1295
|
+
extensions: "paths"
|
|
1296
|
+
};
|
|
1297
|
+
var API_SURFACE_KEYS = {
|
|
1298
|
+
out: "path-or-null",
|
|
1299
|
+
entry: "path",
|
|
1300
|
+
stabilityTags: "names"
|
|
1301
|
+
};
|
|
1302
|
+
var CALL_NAME = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
1303
|
+
function isObject(value) {
|
|
1304
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1468
1305
|
}
|
|
1469
|
-
function
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1306
|
+
function checkValue(key, kind, value, fail) {
|
|
1307
|
+
const text = (v, what) => {
|
|
1308
|
+
if (typeof v !== "string" || v === "")
|
|
1309
|
+
throw fail(`'${key}' must be ${what}`);
|
|
1310
|
+
return v;
|
|
1311
|
+
};
|
|
1312
|
+
const relPath = (v, what) => {
|
|
1313
|
+
const p = text(v, what);
|
|
1314
|
+
if (isAbsolutePath(p))
|
|
1315
|
+
throw fail(`'${key}' ${ABSOLUTE_PATH_TEXT}`);
|
|
1316
|
+
return p;
|
|
1317
|
+
};
|
|
1318
|
+
const list = (v, item, what) => {
|
|
1319
|
+
if (!Array.isArray(v))
|
|
1320
|
+
throw fail(`'${key}' must be ${what}`);
|
|
1321
|
+
return v.map(item);
|
|
1322
|
+
};
|
|
1323
|
+
switch (kind) {
|
|
1324
|
+
case "path":
|
|
1325
|
+
return relPath(value, "a non-empty relative path");
|
|
1326
|
+
case "path-or-null":
|
|
1327
|
+
return value === null ? null : relPath(value, "a non-empty relative path or null");
|
|
1328
|
+
case "paths":
|
|
1329
|
+
return list(value, (x) => relPath(x, "a list of non-empty relative paths"), "a list of non-empty relative paths");
|
|
1330
|
+
case "src":
|
|
1331
|
+
if (value === "auto")
|
|
1332
|
+
return value;
|
|
1333
|
+
return list(value, (x) => relPath(x, '"auto" or a list of non-empty relative paths'), '"auto" or a list of non-empty relative paths');
|
|
1334
|
+
case "names":
|
|
1335
|
+
return list(value, (x) => text(x, "a list of non-empty strings"), "a list of non-empty strings");
|
|
1336
|
+
case "calls":
|
|
1337
|
+
return list(value, (x) => {
|
|
1338
|
+
if (typeof x !== "string" || !CALL_NAME.test(x)) {
|
|
1339
|
+
throw fail(`'${key}' must be a list of call names (identifiers joined by dots)`);
|
|
1340
|
+
}
|
|
1341
|
+
return x;
|
|
1342
|
+
}, "a list of call names (identifiers joined by dots)");
|
|
1343
|
+
case "boolean":
|
|
1344
|
+
if (typeof value !== "boolean")
|
|
1345
|
+
throw fail(`'${key}' must be true or false`);
|
|
1346
|
+
return value;
|
|
1347
|
+
case "string":
|
|
1348
|
+
return text(value, "a non-empty string");
|
|
1349
|
+
case "string-or-null":
|
|
1350
|
+
return value === null ? null : text(value, "a non-empty string or null");
|
|
1351
|
+
case "count":
|
|
1352
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
1353
|
+
throw fail(`'${key}' must be a whole number of 1 or more`);
|
|
1481
1354
|
}
|
|
1482
|
-
|
|
1355
|
+
return value;
|
|
1356
|
+
case "fraction":
|
|
1357
|
+
if (typeof value !== "number" || !(value >= 0 && value <= 1)) {
|
|
1358
|
+
throw fail(`'${key}' must be a number from 0 to 1`);
|
|
1359
|
+
}
|
|
1360
|
+
return value;
|
|
1483
1361
|
}
|
|
1484
|
-
return { tag: "PLAIN" };
|
|
1485
1362
|
}
|
|
1486
|
-
function
|
|
1487
|
-
const
|
|
1488
|
-
for (const
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
continue;
|
|
1494
|
-
if (key === "types" && file.exports.interfaces.includes(name2))
|
|
1495
|
-
continue;
|
|
1496
|
-
const list = byName.get(name2) ?? [];
|
|
1497
|
-
list.push({ file, category: cat });
|
|
1498
|
-
byName.set(name2, list);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1363
|
+
function checkSection(prefix, object, keys, fail) {
|
|
1364
|
+
const out2 = {};
|
|
1365
|
+
for (const [name2, value] of Object.entries(object)) {
|
|
1366
|
+
const kind = keys[name2];
|
|
1367
|
+
if (kind === undefined)
|
|
1368
|
+
throw fail(`unknown key '${prefix}${name2}'`);
|
|
1369
|
+
out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
|
|
1501
1370
|
}
|
|
1502
|
-
return
|
|
1371
|
+
return out2;
|
|
1503
1372
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1373
|
+
var QUERY_KEYS = {
|
|
1374
|
+
out: "path",
|
|
1375
|
+
nodeRuntimes: "names"
|
|
1376
|
+
};
|
|
1377
|
+
var CLONES_KEYS = {
|
|
1378
|
+
src: "src",
|
|
1379
|
+
out: "path-or-null",
|
|
1380
|
+
minNodes: "count",
|
|
1381
|
+
shingleLength: "count",
|
|
1382
|
+
nearThreshold: "fraction",
|
|
1383
|
+
containmentThreshold: "fraction",
|
|
1384
|
+
containmentMinShingles: "count",
|
|
1385
|
+
rareShingleLimit: "count",
|
|
1386
|
+
minSharedShingles: "count",
|
|
1387
|
+
parseErrorList: "count"
|
|
1388
|
+
};
|
|
1389
|
+
function checkClonesFlag(label, key, value) {
|
|
1390
|
+
return checkValue(label, CLONES_KEYS[key], value, (message) => new Error(message));
|
|
1508
1391
|
}
|
|
1509
|
-
function
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if (!byFile.has(d.file.path))
|
|
1516
|
-
byFile.set(d.file.path, d);
|
|
1517
|
-
if (byFile.size < 2)
|
|
1518
|
-
continue;
|
|
1519
|
-
const categories = new Set;
|
|
1520
|
-
const definers = [];
|
|
1521
|
-
for (const { file, category } of byFile.values()) {
|
|
1522
|
-
categories.add(category);
|
|
1523
|
-
const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw);
|
|
1524
|
-
definers.push({
|
|
1525
|
-
file: file.path,
|
|
1526
|
-
package: file.packageName ?? "unknown",
|
|
1527
|
-
public: isFilePublic(file, name2),
|
|
1528
|
-
tag,
|
|
1529
|
-
...reason ? { reason } : {}
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
definers.sort((a, b) => compareCodeUnits(a.file, b.file));
|
|
1533
|
-
const nonAlias = definers.filter((d) => d.tag !== "ALIAS_DELEGATION");
|
|
1534
|
-
if (nonAlias.length < 2) {
|
|
1535
|
-
entries.push(finalizeDuplicateEntry(name2, categories, definers, "ALIAS_DELEGATION"));
|
|
1392
|
+
function parseConfigSections(parsed, label) {
|
|
1393
|
+
const fail = (message) => new Error(`config ${label}: ${message}`);
|
|
1394
|
+
if (!isObject(parsed))
|
|
1395
|
+
throw fail("the file must hold a JSON object");
|
|
1396
|
+
for (const key of Object.keys(parsed)) {
|
|
1397
|
+
if (key === "$schema")
|
|
1536
1398
|
continue;
|
|
1399
|
+
if (key !== "depgraph" && key !== "map" && key !== "query" && key !== "clones") {
|
|
1400
|
+
throw fail(`unknown key '${key}'`);
|
|
1537
1401
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1402
|
+
}
|
|
1403
|
+
if (parsed.map !== undefined && parsed.depgraph !== undefined) {
|
|
1404
|
+
throw fail("use the section 'map' or its old name 'depgraph', not both");
|
|
1405
|
+
}
|
|
1406
|
+
const sectionName = parsed.map !== undefined ? "map" : "depgraph";
|
|
1407
|
+
const sections = { depgraph: {}, query: {}, clones: {} };
|
|
1408
|
+
const section = parsed[sectionName];
|
|
1409
|
+
if (section !== undefined) {
|
|
1410
|
+
if (!isObject(section))
|
|
1411
|
+
throw fail(`'${sectionName}' must be an object`);
|
|
1412
|
+
const { apiSurface, ...rest } = section;
|
|
1413
|
+
sections.depgraph = checkSection(`${sectionName}.`, rest, DEPGRAPH_KEYS, fail);
|
|
1414
|
+
if (apiSurface !== undefined) {
|
|
1415
|
+
if (!isObject(apiSurface))
|
|
1416
|
+
throw fail(`'${sectionName}.apiSurface' must be an object`);
|
|
1417
|
+
sections.depgraph.apiSurface = checkSection(`${sectionName}.apiSurface.`, apiSurface, API_SURFACE_KEYS, fail);
|
|
1545
1418
|
}
|
|
1546
|
-
entries.push(finalizeDuplicateEntry(name2, categories, definers, entryTag));
|
|
1547
1419
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1420
|
+
const query = parsed.query;
|
|
1421
|
+
if (query !== undefined) {
|
|
1422
|
+
if (!isObject(query))
|
|
1423
|
+
throw fail("'query' must be an object");
|
|
1424
|
+
sections.query = checkSection("query.", query, QUERY_KEYS, fail);
|
|
1425
|
+
}
|
|
1426
|
+
const clones = parsed.clones;
|
|
1427
|
+
if (clones !== undefined) {
|
|
1428
|
+
if (!isObject(clones))
|
|
1429
|
+
throw fail("'clones' must be an object");
|
|
1430
|
+
sections.clones = checkSection("clones.", clones, CLONES_KEYS, fail);
|
|
1431
|
+
}
|
|
1432
|
+
return sections;
|
|
1550
1433
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1434
|
+
function loadConfigSections(root, configPath) {
|
|
1435
|
+
if (configPath !== undefined && isAbsolutePath(configPath)) {
|
|
1436
|
+
throw new Error(`flag --config ${ABSOLUTE_PATH_TEXT}`);
|
|
1437
|
+
}
|
|
1438
|
+
const rel = configPath ?? CONFIG_FILE;
|
|
1439
|
+
const label = `<root>/${rel.replace(/\\/g, "/")}`;
|
|
1440
|
+
const path = resolveUnderRoot(root, rel);
|
|
1441
|
+
if (configPath === undefined && !existsSync2(path))
|
|
1442
|
+
return { depgraph: {}, query: {}, clones: {} };
|
|
1443
|
+
let text;
|
|
1444
|
+
try {
|
|
1445
|
+
text = readFileSync2(path, "utf8");
|
|
1446
|
+
} catch {
|
|
1447
|
+
throw new Error(`config ${label}: the file cannot be read`);
|
|
1448
|
+
}
|
|
1449
|
+
let parsed;
|
|
1450
|
+
try {
|
|
1451
|
+
parsed = JSON.parse(text);
|
|
1452
|
+
} catch {
|
|
1453
|
+
throw new Error(`config ${label}: the file is not valid JSON`);
|
|
1454
|
+
}
|
|
1455
|
+
return parseConfigSections(parsed, label);
|
|
1561
1456
|
}
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
content = readFileSync2(join4(root, relPath), "utf-8");
|
|
1570
|
-
} catch {
|
|
1571
|
-
content = "";
|
|
1572
|
-
}
|
|
1573
|
-
cache.set(relPath, content);
|
|
1574
|
-
}
|
|
1575
|
-
return content;
|
|
1457
|
+
function loadConfigFile(root, configPath) {
|
|
1458
|
+
return loadConfigSections(root, configPath).depgraph;
|
|
1459
|
+
}
|
|
1460
|
+
function mergeQueryConfig(cli, file) {
|
|
1461
|
+
return {
|
|
1462
|
+
out: cli.out ?? file.query.out ?? file.depgraph.out ?? OUTPUT_SUBDIR,
|
|
1463
|
+
nodeRuntimes: cli.nodeRuntimes ?? file.query.nodeRuntimes ?? []
|
|
1576
1464
|
};
|
|
1465
|
+
}
|
|
1466
|
+
var MAP_REGENERATE_COMMAND = "repo-tools map";
|
|
1467
|
+
function mergeMapConfig(cli, file) {
|
|
1468
|
+
const merged = mergeDepgraphConfig(cli, file);
|
|
1469
|
+
const inDocs = (name2) => `${OUTPUT_SUBDIR}/${name2}`;
|
|
1577
1470
|
return {
|
|
1578
|
-
|
|
1579
|
-
|
|
1471
|
+
...merged,
|
|
1472
|
+
duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inDocs("duplicate-allowlist.json"),
|
|
1473
|
+
duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inDocs("duplicate-baseline.json"),
|
|
1474
|
+
coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inDocs("coverage-policy.json"),
|
|
1475
|
+
regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? MAP_REGENERATE_COMMAND
|
|
1580
1476
|
};
|
|
1581
1477
|
}
|
|
1582
|
-
function
|
|
1583
|
-
const
|
|
1584
|
-
const
|
|
1478
|
+
function mergeDepgraphConfig(cli, file) {
|
|
1479
|
+
const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
|
|
1480
|
+
const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
|
|
1585
1481
|
return {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1482
|
+
src: cli.src ?? file.src ?? "auto",
|
|
1483
|
+
tests: cli.tests ?? file.tests ?? [...TEST_DIR_NAMES],
|
|
1484
|
+
out: out2,
|
|
1485
|
+
exclude: cli.exclude ?? file.exclude ?? [...DEFAULT_EXCLUDE],
|
|
1486
|
+
alsoExclude: cli.alsoExclude ?? file.alsoExclude ?? [],
|
|
1487
|
+
strictOrphans: cli.strictOrphans ?? file.strictOrphans ?? false,
|
|
1488
|
+
dispatchCalls: cli.dispatchCalls ?? file.dispatchCalls ?? [...DEFAULT_DISPATCH_CALLS],
|
|
1489
|
+
duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
|
|
1490
|
+
duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
|
|
1491
|
+
coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
|
|
1492
|
+
regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
|
|
1493
|
+
verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
|
|
1494
|
+
apiSurface: {
|
|
1495
|
+
out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
|
|
1496
|
+
entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
|
|
1497
|
+
stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
|
|
1592
1498
|
},
|
|
1593
|
-
|
|
1594
|
-
types: dup.types
|
|
1499
|
+
extensions: cli.extensions ?? file.extensions ?? []
|
|
1595
1500
|
};
|
|
1596
1501
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1502
|
+
|
|
1503
|
+
// src/depgraph/args.ts
|
|
1504
|
+
var SCAN_SCOPE_FLAGS = {
|
|
1505
|
+
"--src": "the census finds the source files itself; the area of a file comes from its path",
|
|
1506
|
+
"--tests": "a test file is found by its name or by its test/ or tests/ folder",
|
|
1507
|
+
"--exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
|
|
1508
|
+
"--also-exclude": "the census reads the files that git tracks; list a file in .gitignore instead",
|
|
1509
|
+
"--all": "the graph always holds every file; the disposition of each file shows reachability",
|
|
1510
|
+
"-a": "the graph always holds every file; the disposition of each file shows reachability",
|
|
1511
|
+
"--reachable-only": "the graph always holds every file; the disposition shows reachability",
|
|
1512
|
+
"--include-tests": "the test coverage reports are always written",
|
|
1513
|
+
"-t": "the test coverage reports are always written"
|
|
1514
|
+
};
|
|
1515
|
+
function scanScopeText(flag) {
|
|
1516
|
+
return `flag ${flag} has no effect in 2.0.0: ${SCAN_SCOPE_FLAGS[flag]}`;
|
|
1604
1517
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1518
|
+
var BOOLEAN_FLAGS = {
|
|
1519
|
+
"--all": "all",
|
|
1520
|
+
"-a": "all",
|
|
1521
|
+
"--include-tests": "includeTests",
|
|
1522
|
+
"-t": "includeTests",
|
|
1523
|
+
"--reachable-only": "reachableOnly",
|
|
1524
|
+
"--strict-orphans": "strictOrphans",
|
|
1525
|
+
"--strict-census": "strictCensus",
|
|
1526
|
+
"--check-census": "checkCensus",
|
|
1527
|
+
"--check-duplicates": "checkDuplicates",
|
|
1528
|
+
"--no-regen": "noRegen",
|
|
1529
|
+
"--write-duplicate-baseline": "writeDuplicateBaseline",
|
|
1530
|
+
"--no-extensions": "noExtensions",
|
|
1531
|
+
"--help": "help",
|
|
1532
|
+
"-h": "help"
|
|
1533
|
+
};
|
|
1534
|
+
var VALUE_FLAGS = {
|
|
1535
|
+
"--root": (_o, value, setRoot) => setRoot(value),
|
|
1536
|
+
"--config": (o, value) => {
|
|
1537
|
+
o.config = relativePath("--config", value);
|
|
1538
|
+
},
|
|
1539
|
+
"--src": (o, value) => {
|
|
1540
|
+
o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
|
|
1541
|
+
},
|
|
1542
|
+
"--tests": (o, value) => {
|
|
1543
|
+
o.settings.tests = pathList("--tests", value);
|
|
1544
|
+
},
|
|
1545
|
+
"--out": (o, value) => {
|
|
1546
|
+
o.settings.out = relativePath("--out", value);
|
|
1547
|
+
},
|
|
1548
|
+
"--exclude": (o, value) => {
|
|
1549
|
+
o.settings.exclude = list("--exclude", value);
|
|
1550
|
+
},
|
|
1551
|
+
"--also-exclude": (o, value) => {
|
|
1552
|
+
o.settings.alsoExclude = list("--also-exclude", value);
|
|
1553
|
+
},
|
|
1554
|
+
"--api-surface": (o, value) => {
|
|
1555
|
+
o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
|
|
1556
|
+
},
|
|
1557
|
+
"--api-entry": (o, value) => {
|
|
1558
|
+
o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
|
|
1559
|
+
},
|
|
1560
|
+
"--stability-tags": (o, value) => {
|
|
1561
|
+
o.settings.apiSurface = {
|
|
1562
|
+
...o.settings.apiSurface,
|
|
1563
|
+
stabilityTags: list("--stability-tags", value)
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
function list(flag, value) {
|
|
1568
|
+
const items = value.split(",");
|
|
1569
|
+
if (items.some((item) => item === ""))
|
|
1570
|
+
throw new Error(`flag ${flag} holds an empty item`);
|
|
1571
|
+
return items;
|
|
1611
1572
|
}
|
|
1612
|
-
function
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1573
|
+
function pathList(flag, value) {
|
|
1574
|
+
return list(flag, value).map((item) => relativePath(flag, item));
|
|
1575
|
+
}
|
|
1576
|
+
function relativePath(flag, value) {
|
|
1577
|
+
if (isAbsolutePath(value)) {
|
|
1578
|
+
throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
|
|
1579
|
+
}
|
|
1580
|
+
return value;
|
|
1581
|
+
}
|
|
1582
|
+
function parseDepgraphArgs(argv, cwd, command) {
|
|
1583
|
+
const options = {
|
|
1584
|
+
root: cwd,
|
|
1585
|
+
settings: {},
|
|
1586
|
+
includeTests: false,
|
|
1587
|
+
all: false,
|
|
1588
|
+
reachableOnly: false,
|
|
1589
|
+
strictOrphans: false,
|
|
1590
|
+
strictCensus: false,
|
|
1591
|
+
checkCensus: false,
|
|
1592
|
+
checkDuplicates: false,
|
|
1593
|
+
noRegen: false,
|
|
1594
|
+
writeDuplicateBaseline: false,
|
|
1595
|
+
noExtensions: false,
|
|
1596
|
+
help: false,
|
|
1597
|
+
ignoredFlags: []
|
|
1598
|
+
};
|
|
1599
|
+
let rootSet = false;
|
|
1600
|
+
const setRoot = (value) => {
|
|
1601
|
+
if (rootSet)
|
|
1602
|
+
throw new Error("the root is set twice (use --root=<path> or one positional path)");
|
|
1603
|
+
rootSet = true;
|
|
1604
|
+
options.root = value;
|
|
1605
|
+
};
|
|
1606
|
+
for (const arg of argv) {
|
|
1607
|
+
if (!arg.startsWith("-")) {
|
|
1608
|
+
setRoot(arg);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const eq = arg.indexOf("=");
|
|
1612
|
+
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
1613
|
+
const value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
1614
|
+
if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
|
|
1615
|
+
if (command === "map")
|
|
1616
|
+
throw new Error(scanScopeText(name2));
|
|
1617
|
+
const takesValue = name2 in VALUE_FLAGS;
|
|
1618
|
+
if (!takesValue && value !== undefined)
|
|
1619
|
+
throw new Error(`flag ${name2} takes no value`);
|
|
1620
|
+
if (takesValue && (value === undefined || value === "")) {
|
|
1621
|
+
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1622
|
+
}
|
|
1623
|
+
options.ignoredFlags.push(name2);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const booleanKey = BOOLEAN_FLAGS[name2];
|
|
1627
|
+
if (booleanKey !== undefined) {
|
|
1628
|
+
if (value !== undefined)
|
|
1629
|
+
throw new Error(`flag ${name2} takes no value`);
|
|
1630
|
+
options[booleanKey] = true;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
const apply = VALUE_FLAGS[name2];
|
|
1634
|
+
if (apply !== undefined) {
|
|
1635
|
+
if (value === undefined || value === "") {
|
|
1636
|
+
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1637
|
+
}
|
|
1638
|
+
apply(options, value, setRoot);
|
|
1639
|
+
continue;
|
|
1619
1640
|
}
|
|
1641
|
+
throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
|
|
1620
1642
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
function isRecord(value) {
|
|
1624
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1643
|
+
checkModes(options);
|
|
1644
|
+
return options;
|
|
1625
1645
|
}
|
|
1626
|
-
function
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1646
|
+
function checkModes(options) {
|
|
1647
|
+
if (options.noRegen && !options.checkDuplicates) {
|
|
1648
|
+
throw new Error("flag --no-regen applies with --check-duplicates only");
|
|
1649
|
+
}
|
|
1650
|
+
const modes = [
|
|
1651
|
+
options.checkCensus && "--check-census",
|
|
1652
|
+
options.checkDuplicates && "--check-duplicates",
|
|
1653
|
+
options.writeDuplicateBaseline && "--write-duplicate-baseline"
|
|
1654
|
+
].filter((mode) => mode !== false);
|
|
1655
|
+
if (modes.length > 1) {
|
|
1656
|
+
throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
|
|
1629
1657
|
}
|
|
1630
|
-
const valid = (e) => isRecord(e) && typeof e.name === "string" && typeof e.tag === "string" && Array.isArray(e.definers) && e.definers.every((d) => isRecord(d) && typeof d.file === "string");
|
|
1631
|
-
if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
|
|
1632
|
-
return null;
|
|
1633
|
-
return parsed;
|
|
1634
1658
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1637
|
-
return null;
|
|
1638
|
-
const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
|
|
1639
|
-
const runtime = kind(parsed.runtime);
|
|
1640
|
-
const types = kind(parsed.types);
|
|
1641
|
-
return runtime && types ? { runtime, types } : null;
|
|
1659
|
+
function wantsHelp(argv) {
|
|
1660
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
1642
1661
|
}
|
|
1643
1662
|
|
|
1663
|
+
// src/map/command.ts
|
|
1664
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync15, statSync as statSync6 } from "node:fs";
|
|
1665
|
+
import { resolve as resolve5 } from "node:path";
|
|
1666
|
+
|
|
1644
1667
|
// src/depgraph/extensions.ts
|
|
1645
|
-
import { existsSync as existsSync3,
|
|
1646
|
-
import {
|
|
1668
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
1669
|
+
import { isAbsolute, relative as relative2, resolve as resolve3, sep } from "node:path";
|
|
1647
1670
|
import { pathToFileURL } from "node:url";
|
|
1648
1671
|
|
|
1649
1672
|
// src/io.ts
|
|
1650
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
1651
|
-
import { dirname } from "node:path";
|
|
1673
|
+
import { lstatSync as lstatSync2, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
1674
|
+
import { basename, dirname, join as join5, resolve as resolve2 } from "node:path";
|
|
1652
1675
|
function writeLf(path, text) {
|
|
1653
1676
|
mkdirSync(dirname(path), { recursive: true });
|
|
1654
1677
|
writeFileSync(path, text.replace(/\r\n?/g, `
|
|
@@ -1666,6 +1689,22 @@ function toJson(value) {
|
|
|
1666
1689
|
return `${JSON.stringify(value, null, 2)}
|
|
1667
1690
|
`;
|
|
1668
1691
|
}
|
|
1692
|
+
function realPath(path) {
|
|
1693
|
+
let existing = resolve2(path);
|
|
1694
|
+
const rest = [];
|
|
1695
|
+
while (!lstatSync2(existing, { throwIfNoEntry: false })) {
|
|
1696
|
+
const up = dirname(existing);
|
|
1697
|
+
if (up === existing)
|
|
1698
|
+
return;
|
|
1699
|
+
rest.unshift(basename(existing));
|
|
1700
|
+
existing = up;
|
|
1701
|
+
}
|
|
1702
|
+
try {
|
|
1703
|
+
return join5(realpathSync.native(existing), ...rest);
|
|
1704
|
+
} catch {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1669
1708
|
|
|
1670
1709
|
// src/depgraph/extensions.ts
|
|
1671
1710
|
var MASK = Object.freeze({ blankCommentsAndStrings, stripComments });
|
|
@@ -1746,27 +1785,11 @@ function outsideText(folder, path) {
|
|
|
1746
1785
|
const rel = relative2(folder, path);
|
|
1747
1786
|
return rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("../") || isAbsolute(rel);
|
|
1748
1787
|
}
|
|
1749
|
-
function realPath(path) {
|
|
1750
|
-
let existing = resolve2(path);
|
|
1751
|
-
const rest = [];
|
|
1752
|
-
while (!lstatSync2(existing, { throwIfNoEntry: false })) {
|
|
1753
|
-
const up = dirname2(existing);
|
|
1754
|
-
if (up === existing)
|
|
1755
|
-
return;
|
|
1756
|
-
rest.unshift(basename(existing));
|
|
1757
|
-
existing = up;
|
|
1758
|
-
}
|
|
1759
|
-
try {
|
|
1760
|
-
return join5(realpathSync.native(existing), ...rest);
|
|
1761
|
-
} catch {
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
1788
|
function outputPath(outputDir, relPath) {
|
|
1766
1789
|
if (typeof relPath !== "string" || relPath === "" || isAbsolutePath(relPath)) {
|
|
1767
1790
|
throw new Error("write: the path must be relative to the output folder");
|
|
1768
1791
|
}
|
|
1769
|
-
const path =
|
|
1792
|
+
const path = resolve3(outputDir, relPath);
|
|
1770
1793
|
if (outsideText(outputDir, path)) {
|
|
1771
1794
|
throw new Error("write: the path must stay in the output folder");
|
|
1772
1795
|
}
|
|
@@ -1779,8 +1802,8 @@ function outputPath(outputDir, relPath) {
|
|
|
1779
1802
|
}
|
|
1780
1803
|
|
|
1781
1804
|
// src/map/artifacts.ts
|
|
1782
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
1783
|
-
import { basename as basename4, dirname as
|
|
1805
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1806
|
+
import { basename as basename4, dirname as dirname5, join as join13 } from "node:path";
|
|
1784
1807
|
|
|
1785
1808
|
// src/map/cycles.ts
|
|
1786
1809
|
function sccContaining(start2, eligible, edges, reverseEdges) {
|
|
@@ -1920,7 +1943,7 @@ function stronglyConnectedComponents(graph) {
|
|
|
1920
1943
|
}
|
|
1921
1944
|
|
|
1922
1945
|
// src/depgraph/resolver.ts
|
|
1923
|
-
import { dirname as
|
|
1946
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
1924
1947
|
function distToSrc(path) {
|
|
1925
1948
|
const m = /(^|\/)dist\/(?:src\/)?/.exec(path);
|
|
1926
1949
|
if (!m)
|
|
@@ -1931,7 +1954,7 @@ function distToSrc(path) {
|
|
|
1931
1954
|
return `${before}${m[1]}src/${path.slice(m.index + m[0].length)}`;
|
|
1932
1955
|
}
|
|
1933
1956
|
function resolveCandidates(fromPath, spec) {
|
|
1934
|
-
const base = toPosix(join6(
|
|
1957
|
+
const base = toPosix(join6(dirname2(fromPath), spec));
|
|
1935
1958
|
let candidates;
|
|
1936
1959
|
if (base.endsWith(".js")) {
|
|
1937
1960
|
const stem = base.slice(0, -3);
|
|
@@ -2065,7 +2088,7 @@ function filesInCycles(components) {
|
|
|
2065
2088
|
|
|
2066
2089
|
// src/depgraph/roots.ts
|
|
2067
2090
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
2068
|
-
import { dirname as
|
|
2091
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
2069
2092
|
function isJsonObject(value) {
|
|
2070
2093
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2071
2094
|
}
|
|
@@ -2148,7 +2171,7 @@ function seedTsconfigEntries(root, pkgDir, cfgRel, add) {
|
|
|
2148
2171
|
} catch {
|
|
2149
2172
|
return;
|
|
2150
2173
|
}
|
|
2151
|
-
const cfgDir =
|
|
2174
|
+
const cfgDir = dirname3(join7(pkgDir, cfgRel));
|
|
2152
2175
|
const seedPath = (p) => {
|
|
2153
2176
|
if (p.includes("*")) {
|
|
2154
2177
|
const base = join7(root, cfgDir, p.replace(/\/?\*.*$/, ""));
|
|
@@ -2342,7 +2365,7 @@ function htmlReferencedEntries(root) {
|
|
|
2342
2365
|
const src = m[1] ?? "";
|
|
2343
2366
|
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src) || !/\.[cm]?[jt]sx?$/.test(src))
|
|
2344
2367
|
continue;
|
|
2345
|
-
const fromPage = toPosix(join7(
|
|
2368
|
+
const fromPage = toPosix(join7(dirname3(page), src)).replace(/^\.\//, "");
|
|
2346
2369
|
const candidates = src.startsWith("/") ? [src.slice(1), fromPage] : [fromPage];
|
|
2347
2370
|
const found = candidates.find((c) => existsSync4(join7(root, c)));
|
|
2348
2371
|
if (found !== undefined && !out2.includes(found))
|
|
@@ -2362,7 +2385,7 @@ function runtimeLaunchedEntries(root, parsed) {
|
|
|
2362
2385
|
continue;
|
|
2363
2386
|
}
|
|
2364
2387
|
for (const m of code.matchAll(/new\s+URL\(\s*['"`](\.[^'"`]*?\.m?js)['"`]\s*,\s*import\.meta\.url/g)) {
|
|
2365
|
-
const sibling = toPosix(join7(
|
|
2388
|
+
const sibling = toPosix(join7(dirname3(file.path), m[1] ?? ""));
|
|
2366
2389
|
const tsPath = sibling.replace(/\.m?js$/, ".ts");
|
|
2367
2390
|
if (known.has(tsPath))
|
|
2368
2391
|
out2.add(tsPath);
|
|
@@ -6228,7 +6251,7 @@ import { readFileSync as readFileSync5 } from "node:fs";
|
|
|
6228
6251
|
import { join as join9, posix as posix2 } from "node:path";
|
|
6229
6252
|
|
|
6230
6253
|
// src/depgraph/description.ts
|
|
6231
|
-
import { basename as basename2, dirname as
|
|
6254
|
+
import { basename as basename2, dirname as dirname4 } from "node:path";
|
|
6232
6255
|
var RULE_LINE = /^[=\-*~#_]{3,}$/;
|
|
6233
6256
|
function extractDescription(content) {
|
|
6234
6257
|
const jsdocMatch = content.match(/\/\*\*\s*\n([^*]*(?:\*(?!\/)[^*]*)*)\*\//);
|
|
@@ -6263,7 +6286,7 @@ function generateFallbackDescription(file) {
|
|
|
6263
6286
|
const fileName = withoutTsExtension(basename2(file.path));
|
|
6264
6287
|
if (fileName === "index") {
|
|
6265
6288
|
if (file.exports.reExported.length > 0) {
|
|
6266
|
-
const pkgName = file.packageName ||
|
|
6289
|
+
const pkgName = file.packageName || dirname4(file.path).split("/").pop() || "";
|
|
6267
6290
|
return `Package entry point for ${pkgName || "module"} (re-exports ${file.exports.reExported.length} symbols)`;
|
|
6268
6291
|
}
|
|
6269
6292
|
if (file.exports.named.length > 0) {
|
|
@@ -6477,6 +6500,26 @@ function isReparsePoint(path) {
|
|
|
6477
6500
|
return false;
|
|
6478
6501
|
}
|
|
6479
6502
|
}
|
|
6503
|
+
function rglobParts(root, suffix) {
|
|
6504
|
+
const found = [];
|
|
6505
|
+
const go = (dir, parts2) => {
|
|
6506
|
+
let entries;
|
|
6507
|
+
try {
|
|
6508
|
+
entries = readdirHook.entries(dir);
|
|
6509
|
+
} catch {
|
|
6510
|
+
return;
|
|
6511
|
+
}
|
|
6512
|
+
for (const e of entries) {
|
|
6513
|
+
const full = join10(dir, e.name);
|
|
6514
|
+
if (e.isDirectory() && !isReparsePoint(full))
|
|
6515
|
+
go(full, [...parts2, e.name]);
|
|
6516
|
+
else if (e.name.endsWith(suffix))
|
|
6517
|
+
found.push([...parts2, e.name]);
|
|
6518
|
+
}
|
|
6519
|
+
};
|
|
6520
|
+
go(root, []);
|
|
6521
|
+
return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
|
|
6522
|
+
}
|
|
6480
6523
|
function entries(dir) {
|
|
6481
6524
|
try {
|
|
6482
6525
|
return readdirHook.entries(dir);
|
|
@@ -6698,8 +6741,8 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
|
|
|
6698
6741
|
}
|
|
6699
6742
|
|
|
6700
6743
|
// src/map/graph.ts
|
|
6701
|
-
import { readFileSync as
|
|
6702
|
-
import { basename as basename3, join as
|
|
6744
|
+
import { readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
|
|
6745
|
+
import { basename as basename3, join as join12, resolve as resolve4 } from "node:path";
|
|
6703
6746
|
|
|
6704
6747
|
// node_modules/smol-toml/dist/error.js
|
|
6705
6748
|
/*!
|
|
@@ -11007,6 +11050,20 @@ function parserFor(name2) {
|
|
|
11007
11050
|
return parser;
|
|
11008
11051
|
}
|
|
11009
11052
|
|
|
11053
|
+
// src/map/package-field.ts
|
|
11054
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
11055
|
+
import { join as join11 } from "node:path";
|
|
11056
|
+
function packageField(root, key) {
|
|
11057
|
+
let pkg = null;
|
|
11058
|
+
try {
|
|
11059
|
+
pkg = JSON.parse(readFileSync8(join11(root, "package.json"), "utf8"));
|
|
11060
|
+
} catch {}
|
|
11061
|
+
if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
|
|
11062
|
+
return null;
|
|
11063
|
+
const value = pkg[key];
|
|
11064
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
11065
|
+
}
|
|
11066
|
+
|
|
11010
11067
|
// src/map/parsing.ts
|
|
11011
11068
|
function emptyModule() {
|
|
11012
11069
|
return {
|
|
@@ -11074,6 +11131,7 @@ function importClauseNames(clause) {
|
|
|
11074
11131
|
return names;
|
|
11075
11132
|
}
|
|
11076
11133
|
var hasChild = (node, type) => node.children.some((c) => c.type === type);
|
|
11134
|
+
var allInlineTyped = (specifiers) => specifiers.length > 0 && specifiers.every((s) => hasChild(s, "type"));
|
|
11077
11135
|
function isTypeOnlyImport(stmt, clause) {
|
|
11078
11136
|
if (hasChild(stmt, "type"))
|
|
11079
11137
|
return true;
|
|
@@ -11082,10 +11140,7 @@ function isTypeOnlyImport(stmt, clause) {
|
|
|
11082
11140
|
const kids = clause.children.filter((c) => c.type !== ",");
|
|
11083
11141
|
if (kids.length !== 1 || kids[0]?.type !== "named_imports")
|
|
11084
11142
|
return false;
|
|
11085
|
-
|
|
11086
|
-
if (specifiers.length === 0)
|
|
11087
|
-
return false;
|
|
11088
|
-
return specifiers.every((s) => hasChild(s, "type"));
|
|
11143
|
+
return allInlineTyped(kids[0].children.filter((c) => c.type === "import_specifier"));
|
|
11089
11144
|
}
|
|
11090
11145
|
function defaultExportKindAndLocal(stmt) {
|
|
11091
11146
|
for (const c of stmt.children) {
|
|
@@ -11150,11 +11205,10 @@ function exportClauseSourceNames(clause) {
|
|
|
11150
11205
|
function isTypeOnlyExport(stmt, clause) {
|
|
11151
11206
|
if (hasChild(stmt, "type"))
|
|
11152
11207
|
return true;
|
|
11153
|
-
|
|
11154
|
-
if (specifiers.length === 0)
|
|
11155
|
-
return false;
|
|
11156
|
-
return specifiers.every((s) => hasChild(s, "type"));
|
|
11208
|
+
return allInlineTyped(clause.children.filter((c) => c.type === "export_specifier"));
|
|
11157
11209
|
}
|
|
11210
|
+
var GAP = String.raw`(?:\s|/\*[\s\S]*?\*/)`;
|
|
11211
|
+
var EXPORT_TYPE_STAR = new RegExp(String.raw`^export${GAP}+type${GAP}*\*`, "u");
|
|
11158
11212
|
function stripChars(text, chars) {
|
|
11159
11213
|
let a = 0;
|
|
11160
11214
|
let b = text.length;
|
|
@@ -11273,6 +11327,7 @@ function parseTs(source, path = "") {
|
|
|
11273
11327
|
const nsExport = node.children.find((c) => c.type === "namespace_export");
|
|
11274
11328
|
let names = [];
|
|
11275
11329
|
let typeOnly = false;
|
|
11330
|
+
const typeStar = hasChild(node, "type") || EXPORT_TYPE_STAR.test(node.text);
|
|
11276
11331
|
if (clause) {
|
|
11277
11332
|
names = exportClauseSourceNames(clause);
|
|
11278
11333
|
typeOnly = isTypeOnlyExport(node, clause);
|
|
@@ -11280,11 +11335,12 @@ function parseTs(source, path = "") {
|
|
|
11280
11335
|
mod.reExports.push(name2);
|
|
11281
11336
|
} else if (nsExport) {
|
|
11282
11337
|
names = ["*"];
|
|
11338
|
+
typeOnly = typeStar;
|
|
11283
11339
|
for (const [name2] of exportEntries(node))
|
|
11284
11340
|
mod.reExports.push(name2);
|
|
11285
11341
|
} else {
|
|
11286
|
-
|
|
11287
|
-
mod.reExports.push(`${typeStar}* from ${specifierOf(spec)}`);
|
|
11342
|
+
typeOnly = typeStar;
|
|
11343
|
+
mod.reExports.push(`${typeStar ? "type " : ""}* from ${specifierOf(spec)}`);
|
|
11288
11344
|
}
|
|
11289
11345
|
mod.imports.push({ specifier: specifierOf(spec), names, typeOnly, reExport: true });
|
|
11290
11346
|
}
|
|
@@ -12392,7 +12448,7 @@ var STRING_LITERAL = /['"]([^'"\n]*)['"]/g;
|
|
|
12392
12448
|
var PATH_SEGMENT = new RegExp(`^(?:${W}|[.-])+$`, "u");
|
|
12393
12449
|
var lstripDotSlash = (text) => text.replace(/^[./]+/, "");
|
|
12394
12450
|
function launcherEntry(root, spec, known) {
|
|
12395
|
-
const path =
|
|
12451
|
+
const path = join12(root, lstripDotSlash(spec.replaceAll("\\", "/")));
|
|
12396
12452
|
let text;
|
|
12397
12453
|
try {
|
|
12398
12454
|
const st = statSync4(path);
|
|
@@ -12440,7 +12496,7 @@ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
12440
12496
|
function packageJsonRoots(root, known) {
|
|
12441
12497
|
let pkg;
|
|
12442
12498
|
try {
|
|
12443
|
-
pkg = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
12499
|
+
pkg = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync9(join12(root, "package.json"))));
|
|
12444
12500
|
} catch {
|
|
12445
12501
|
return [[], []];
|
|
12446
12502
|
}
|
|
@@ -12488,7 +12544,7 @@ var PYTHON_FALLBACK_ROOTS = [
|
|
|
12488
12544
|
];
|
|
12489
12545
|
function pythonRoots(root, known) {
|
|
12490
12546
|
const warnings = [];
|
|
12491
|
-
const pyproject =
|
|
12547
|
+
const pyproject = join12(root, "pyproject.toml");
|
|
12492
12548
|
let isFile = false;
|
|
12493
12549
|
try {
|
|
12494
12550
|
isFile = statSync4(pyproject).isFile();
|
|
@@ -12539,31 +12595,11 @@ function pythonRoots(root, known) {
|
|
|
12539
12595
|
}
|
|
12540
12596
|
var CSHARP_MAIN = new RegExp(`${B}static${S}+(?:async${S}+)?[^${SPACE_BODY}(){};]+${S}+Main${S}*\\(`, "u");
|
|
12541
12597
|
var CSPROJ_EXE = new RegExp(`<OutputType>${S}*(Exe|WinExe)${S}*</OutputType>`, "iu");
|
|
12542
|
-
function rglobParts(root, suffix) {
|
|
12543
|
-
const found = [];
|
|
12544
|
-
const go = (dir, parts2) => {
|
|
12545
|
-
let entries;
|
|
12546
|
-
try {
|
|
12547
|
-
entries = readdirHook.entries(dir);
|
|
12548
|
-
} catch {
|
|
12549
|
-
return;
|
|
12550
|
-
}
|
|
12551
|
-
for (const e of entries) {
|
|
12552
|
-
const full = join11(dir, e.name);
|
|
12553
|
-
if (e.isDirectory() && !isReparsePoint(full))
|
|
12554
|
-
go(full, [...parts2, e.name]);
|
|
12555
|
-
else if (e.name.endsWith(suffix))
|
|
12556
|
-
found.push([...parts2, e.name]);
|
|
12557
|
-
}
|
|
12558
|
-
};
|
|
12559
|
-
go(root, []);
|
|
12560
|
-
return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
|
|
12561
|
-
}
|
|
12562
12598
|
function csharpRoots(root, known) {
|
|
12563
12599
|
const warnings = [];
|
|
12564
12600
|
const declaresMain = (rel) => {
|
|
12565
12601
|
try {
|
|
12566
|
-
return CSHARP_MAIN.test(readSource(
|
|
12602
|
+
return CSHARP_MAIN.test(readSource(join12(root, rel)));
|
|
12567
12603
|
} catch {
|
|
12568
12604
|
return false;
|
|
12569
12605
|
}
|
|
@@ -12574,7 +12610,7 @@ function csharpRoots(root, known) {
|
|
|
12574
12610
|
continue;
|
|
12575
12611
|
let text;
|
|
12576
12612
|
try {
|
|
12577
|
-
text = readSource(
|
|
12613
|
+
text = readSource(join12(root, ...parts2));
|
|
12578
12614
|
} catch (error) {
|
|
12579
12615
|
warnings.push(`could not read ${parts2.join("/")} (${error instanceof Error ? error.message : String(error)})`);
|
|
12580
12616
|
continue;
|
|
@@ -12596,7 +12632,7 @@ function csharpRoots(root, known) {
|
|
|
12596
12632
|
if (program.length > 0)
|
|
12597
12633
|
roots.push(program[0]);
|
|
12598
12634
|
else {
|
|
12599
|
-
warnings.push(`${projDir || basename3(
|
|
12635
|
+
warnings.push(`${projDir || basename3(resolve4(root))} declares OutputType Exe but no file in it declares ` + "Main and none is named Program.cs; it contributes no root");
|
|
12600
12636
|
}
|
|
12601
12637
|
}
|
|
12602
12638
|
if (roots.length > 0)
|
|
@@ -12632,7 +12668,7 @@ function workspaceRoots(root, known) {
|
|
|
12632
12668
|
continue;
|
|
12633
12669
|
const prefix = `${ws.directory}/`;
|
|
12634
12670
|
const local = new Set([...known].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)));
|
|
12635
|
-
const [found, pkgWarnings] = packageJsonRoots(
|
|
12671
|
+
const [found, pkgWarnings] = packageJsonRoots(join12(root, ws.directory), local);
|
|
12636
12672
|
const pkgRoots = found.map((p) => prefix + p);
|
|
12637
12673
|
for (const entry of ws.extraEntries)
|
|
12638
12674
|
if (known.has(entry))
|
|
@@ -12681,7 +12717,7 @@ async function buildGraph(root) {
|
|
|
12681
12717
|
const parsed = new Map;
|
|
12682
12718
|
const sources = new Map;
|
|
12683
12719
|
for (const f of found) {
|
|
12684
|
-
const text = readSource(
|
|
12720
|
+
const text = readSource(join12(root, f.path)).replace(/\r\n?/g, `
|
|
12685
12721
|
`);
|
|
12686
12722
|
sources.set(f.path, text);
|
|
12687
12723
|
parsed.set(f.path, read(text, f.path));
|
|
@@ -12811,7 +12847,7 @@ async function buildGraph(root) {
|
|
|
12811
12847
|
warnings.push("could not determine any entry-point roots: no package.json main/module/bin/exports resolved " + "to a known source file, and no src/index.* fallback was found -- every 'src' file will show " + "as orphan or test-only rather than build-entry");
|
|
12812
12848
|
}
|
|
12813
12849
|
const graph = newRepoGraph({
|
|
12814
|
-
name: basename3(
|
|
12850
|
+
name: packageField(root, "name") ?? basename3(resolve4(root)),
|
|
12815
12851
|
files,
|
|
12816
12852
|
roots,
|
|
12817
12853
|
warnings,
|
|
@@ -12915,7 +12951,7 @@ function pyJsonDumps(data) {
|
|
|
12915
12951
|
return out2;
|
|
12916
12952
|
}
|
|
12917
12953
|
function writeJson(path, data) {
|
|
12918
|
-
mkdirSync2(
|
|
12954
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
12919
12955
|
writeFileSync2(path, `${pyJsonDumps(data)}
|
|
12920
12956
|
`);
|
|
12921
12957
|
return path;
|
|
@@ -12998,11 +13034,11 @@ function emitDependencyGraph(graph, outDir, options = {}) {
|
|
|
12998
13034
|
...graph.language === "typescript" ? exportKindStats(graph) : {}
|
|
12999
13035
|
});
|
|
13000
13036
|
data.warnings = [...graph.warnings];
|
|
13001
|
-
return writeJson(
|
|
13037
|
+
return writeJson(join13(outDir, "dependency-graph.json"), data);
|
|
13002
13038
|
}
|
|
13003
13039
|
function readJson(path) {
|
|
13004
13040
|
try {
|
|
13005
|
-
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
13041
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync10(path)));
|
|
13006
13042
|
} catch {
|
|
13007
13043
|
return null;
|
|
13008
13044
|
}
|
|
@@ -13012,7 +13048,7 @@ var PNPM_ITEM = new RegExp(`(?<=^|\\n)${S}*-${S}*['"]?([^'"${SPACE_BODY}#]+)['"]
|
|
|
13012
13048
|
function pnpmWorkspacePatterns(root) {
|
|
13013
13049
|
let text;
|
|
13014
13050
|
try {
|
|
13015
|
-
text = readSource(
|
|
13051
|
+
text = readSource(join13(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
|
|
13016
13052
|
`);
|
|
13017
13053
|
} catch {
|
|
13018
13054
|
return [];
|
|
@@ -13042,29 +13078,9 @@ function normalizeWorkspacePatterns(raw) {
|
|
|
13042
13078
|
]
|
|
13043
13079
|
];
|
|
13044
13080
|
}
|
|
13045
|
-
function csprojParts(root) {
|
|
13046
|
-
const found = [];
|
|
13047
|
-
const go = (dir, parts2) => {
|
|
13048
|
-
let entries;
|
|
13049
|
-
try {
|
|
13050
|
-
entries = readdirHook.entries(dir);
|
|
13051
|
-
} catch {
|
|
13052
|
-
return;
|
|
13053
|
-
}
|
|
13054
|
-
for (const e of entries) {
|
|
13055
|
-
const full = join12(dir, e.name);
|
|
13056
|
-
if (e.isDirectory() && !isReparsePoint(full))
|
|
13057
|
-
go(full, [...parts2, e.name]);
|
|
13058
|
-
else if (e.name.endsWith(".csproj"))
|
|
13059
|
-
found.push([...parts2, e.name]);
|
|
13060
|
-
}
|
|
13061
|
-
};
|
|
13062
|
-
go(root, []);
|
|
13063
|
-
return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
|
|
13064
|
-
}
|
|
13065
13081
|
function csprojMap(root) {
|
|
13066
13082
|
const projects = new Map;
|
|
13067
|
-
for (const parts2 of
|
|
13083
|
+
for (const parts2 of rglobParts(root, ".csproj")) {
|
|
13068
13084
|
if (parts2.some((p) => p === "obj" || p === "bin"))
|
|
13069
13085
|
continue;
|
|
13070
13086
|
const name2 = parts2.at(-1);
|
|
@@ -13082,13 +13098,13 @@ function csprojMap(root) {
|
|
|
13082
13098
|
}
|
|
13083
13099
|
function subdirs(dir) {
|
|
13084
13100
|
try {
|
|
13085
|
-
return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(
|
|
13101
|
+
return readdirHook.entries(dir).filter((e) => e.isDirectory() || e.isSymbolicLink() && statSync5(join13(dir, e.name)).isDirectory()).map((e) => e.name).sort(compareCodeUnits);
|
|
13086
13102
|
} catch {
|
|
13087
13103
|
return [];
|
|
13088
13104
|
}
|
|
13089
13105
|
}
|
|
13090
13106
|
function workspaceMap(root) {
|
|
13091
|
-
const pkg = readJson(
|
|
13107
|
+
const pkg = readJson(join13(root, "package.json"));
|
|
13092
13108
|
if (!isObject3(pkg)) {
|
|
13093
13109
|
return [
|
|
13094
13110
|
new Map,
|
|
@@ -13122,18 +13138,18 @@ function workspaceMap(root) {
|
|
|
13122
13138
|
const parent = pattern.slice(0, -2);
|
|
13123
13139
|
let isDir = false;
|
|
13124
13140
|
try {
|
|
13125
|
-
isDir = statSync5(
|
|
13141
|
+
isDir = statSync5(join13(root, parent)).isDirectory();
|
|
13126
13142
|
} catch {
|
|
13127
13143
|
isDir = false;
|
|
13128
13144
|
}
|
|
13129
13145
|
if (!isDir)
|
|
13130
13146
|
continue;
|
|
13131
|
-
candidates = subdirs(
|
|
13147
|
+
candidates = subdirs(join13(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
|
|
13132
13148
|
} else {
|
|
13133
13149
|
candidates = [pattern];
|
|
13134
13150
|
}
|
|
13135
13151
|
for (const relCand of candidates) {
|
|
13136
|
-
const sub = readJson(
|
|
13152
|
+
const sub = readJson(join13(root, relCand, "package.json"));
|
|
13137
13153
|
if (!isObject3(sub)) {
|
|
13138
13154
|
warnings.push(`emit_file_inventory: workspace candidate '${relCand}' (matched by '${pattern}') has no readable package.json -- skipped, its files will report package '(root)'`);
|
|
13139
13155
|
continue;
|
|
@@ -13193,7 +13209,7 @@ function emitFileInventory(graph, outDir) {
|
|
|
13193
13209
|
}));
|
|
13194
13210
|
const byDisposition = Object.fromEntries(DISPOSITIONS.map((d) => [d, 0]));
|
|
13195
13211
|
Object.assign(byDisposition, counter(files.map((f) => f.disposition)));
|
|
13196
|
-
return writeJson(
|
|
13212
|
+
return writeJson(join13(outDir, "file-inventory.json"), {
|
|
13197
13213
|
totalFiles: files.length,
|
|
13198
13214
|
byDisposition,
|
|
13199
13215
|
byArea: counter(files.map((f) => f.area)),
|
|
@@ -13227,7 +13243,7 @@ function readSourceText(rootPath, rel) {
|
|
|
13227
13243
|
if (rootPath === null)
|
|
13228
13244
|
return null;
|
|
13229
13245
|
try {
|
|
13230
|
-
return readSource(
|
|
13246
|
+
return readSource(join13(rootPath, rel)).replace(/\r\n?/g, `
|
|
13231
13247
|
`);
|
|
13232
13248
|
} catch {
|
|
13233
13249
|
return null;
|
|
@@ -13382,7 +13398,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
|
|
|
13382
13398
|
duplicates[sym] = paths;
|
|
13383
13399
|
}
|
|
13384
13400
|
const summary = { duplicateCount: Object.keys(duplicates).length, totalSymbols: owners.size };
|
|
13385
|
-
const path =
|
|
13401
|
+
const path = join13(outDir, "duplicate-symbols.json");
|
|
13386
13402
|
const root = graph.rootPath;
|
|
13387
13403
|
if (graph.language !== "typescript" || root === null) {
|
|
13388
13404
|
const reason = root === null ? "This graph has no root folder to read the definitions from." : `This repository reads as ${graph.language}.`;
|
|
@@ -13398,7 +13414,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
|
|
|
13398
13414
|
const rootEntries = workspaces.size > 0 ? [] : rootPackageEntries(root);
|
|
13399
13415
|
const surface = computePublicSurface(records, root, workspaces, rootEntries);
|
|
13400
13416
|
const definers = records.filter((r) => !/\.d\.[cm]?ts$/.test(r.path));
|
|
13401
|
-
const report = buildDuplicateReport(detectDuplicateSymbols(definers, surface, root, options.allowlistPath ??
|
|
13417
|
+
const report = buildDuplicateReport(detectDuplicateSymbols(definers, surface, root, options.allowlistPath ?? join13(root, "docs", "architecture", "duplicate-allowlist.json"), options.dispatchCalls), options.dispatchCalls);
|
|
13402
13418
|
return writeJson(path, {
|
|
13403
13419
|
note: DUPLICATES_CLASSIFIED_NOTE,
|
|
13404
13420
|
classificationNote: report.note,
|
|
@@ -13414,7 +13430,7 @@ function emitUnusedAnalysis(graph, outDir) {
|
|
|
13414
13430
|
const [unreferenced, referencedInModule, unclassified] = classifyUnusedExports(graph);
|
|
13415
13431
|
const notes = unreferencedAnywhereNotes(graph, unreferenced);
|
|
13416
13432
|
const noImporter = noImporterFiles(graph);
|
|
13417
|
-
return writeJson(
|
|
13433
|
+
return writeJson(join13(outDir, "unused-analysis.json"), {
|
|
13418
13434
|
caveats: caveatsFor(graph),
|
|
13419
13435
|
summary: {
|
|
13420
13436
|
unusedExportCount: countAll(unreferenced) + countAll(referencedInModule) + countAll(unclassified),
|
|
@@ -13432,16 +13448,16 @@ function emitUnusedAnalysis(graph, outDir) {
|
|
|
13432
13448
|
}
|
|
13433
13449
|
|
|
13434
13450
|
// src/map/coverage.ts
|
|
13435
|
-
import { join as
|
|
13451
|
+
import { join as join17 } from "node:path";
|
|
13436
13452
|
|
|
13437
13453
|
// src/depgraph/coverage.ts
|
|
13438
|
-
import { existsSync as existsSync6, readFileSync as
|
|
13439
|
-
import { join as
|
|
13454
|
+
import { existsSync as existsSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
13455
|
+
import { join as join14 } from "node:path";
|
|
13440
13456
|
function loadCoveragePolicy(policyPath) {
|
|
13441
13457
|
if (!existsSync6(policyPath))
|
|
13442
13458
|
return null;
|
|
13443
13459
|
try {
|
|
13444
|
-
const parsed = JSON.parse(
|
|
13460
|
+
const parsed = JSON.parse(readFileSync11(policyPath, "utf8"));
|
|
13445
13461
|
if (!parsed.categories || typeof parsed.categories !== "object")
|
|
13446
13462
|
return null;
|
|
13447
13463
|
return parsed;
|
|
@@ -13541,7 +13557,7 @@ function traceReExports(importedPath, reExportMap) {
|
|
|
13541
13557
|
result.add(source);
|
|
13542
13558
|
return result;
|
|
13543
13559
|
}
|
|
13544
|
-
function analyzeTestCoverage(graphFiles, testFiles, root, policyPath =
|
|
13560
|
+
function analyzeTestCoverage(graphFiles, testFiles, root, policyPath = join14(outputDirOf(root), "coverage-policy.json")) {
|
|
13545
13561
|
const sourceFiles = graphFiles.filter((f) => !f.path.endsWith(".d.ts"));
|
|
13546
13562
|
const sourceFilePaths = new Set(sourceFiles.map((f) => f.path));
|
|
13547
13563
|
const coverageMap = new Map;
|
|
@@ -13680,10 +13696,10 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
|
|
|
13680
13696
|
}
|
|
13681
13697
|
lines.push("---");
|
|
13682
13698
|
lines.push("");
|
|
13683
|
-
lines.push("## Source Files
|
|
13699
|
+
lines.push("## Source Files Not Imported By A Test");
|
|
13684
13700
|
lines.push("");
|
|
13685
13701
|
if (coverage.untestedFiles.length === 0) {
|
|
13686
|
-
lines.push("**
|
|
13702
|
+
lines.push("**Every source file is imported by a test file.** \uD83C\uDF89");
|
|
13687
13703
|
} else {
|
|
13688
13704
|
lines.push(`The following ${coverage.untestedFiles.length} source files are not directly imported by any test file:`);
|
|
13689
13705
|
lines.push("");
|
|
@@ -13707,7 +13723,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
|
|
|
13707
13723
|
}
|
|
13708
13724
|
lines.push("---");
|
|
13709
13725
|
lines.push("");
|
|
13710
|
-
lines.push("## Source Files
|
|
13726
|
+
lines.push("## Source Files Imported By A Test");
|
|
13711
13727
|
lines.push("");
|
|
13712
13728
|
lines.push("| Source File | Test Files |");
|
|
13713
13729
|
lines.push("|-------------|------------|");
|
|
@@ -13771,7 +13787,7 @@ function generateTestCoverageJson(coverage, options = {}) {
|
|
|
13771
13787
|
|
|
13772
13788
|
// src/map/reports.ts
|
|
13773
13789
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
13774
|
-
import { join as
|
|
13790
|
+
import { join as join16 } from "node:path";
|
|
13775
13791
|
|
|
13776
13792
|
// src/depgraph/reporters/json.ts
|
|
13777
13793
|
function modulesJsonOf(modules) {
|
|
@@ -13988,10 +14004,14 @@ function fileSection(lines, path, file) {
|
|
|
13988
14004
|
lines.push("---");
|
|
13989
14005
|
lines.push("");
|
|
13990
14006
|
}
|
|
13991
|
-
|
|
14007
|
+
var DEPGRAPH_LABELS = {
|
|
14008
|
+
files: "Total TypeScript Files",
|
|
14009
|
+
modules: "Total Modules"
|
|
14010
|
+
};
|
|
14011
|
+
function summaryRowsOf(stats, labels = DEPGRAPH_LABELS) {
|
|
13992
14012
|
const rows = [
|
|
13993
|
-
[
|
|
13994
|
-
[
|
|
14013
|
+
[labels.files, stats.totalTypeScriptFiles],
|
|
14014
|
+
[labels.modules, stats.totalModules],
|
|
13995
14015
|
["Total Lines of Code", stats.totalLinesOfCode],
|
|
13996
14016
|
["Total Exports", stats.totalExports],
|
|
13997
14017
|
["Total Re-exports", stats.totalReExports],
|
|
@@ -14134,20 +14154,12 @@ function generateYaml(json, dumpFn = dump) {
|
|
|
14134
14154
|
}
|
|
14135
14155
|
|
|
14136
14156
|
// src/map/layers.ts
|
|
14137
|
-
import {
|
|
14138
|
-
import { join as join14 } from "node:path";
|
|
14157
|
+
import { join as join15 } from "node:path";
|
|
14139
14158
|
function projectIdentity(root, graph) {
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
} catch {}
|
|
14144
|
-
const field = (key) => {
|
|
14145
|
-
if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
|
|
14146
|
-
return null;
|
|
14147
|
-
const value = pkg[key];
|
|
14148
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
14159
|
+
return {
|
|
14160
|
+
name: packageField(root, "name") ?? graph.name,
|
|
14161
|
+
version: packageField(root, "version") ?? "unknown"
|
|
14149
14162
|
};
|
|
14150
|
-
return { name: field("name") ?? graph.name, version: field("version") ?? "unknown" };
|
|
14151
14163
|
}
|
|
14152
14164
|
function subsystemView(graph, root, warn = () => {}) {
|
|
14153
14165
|
const records = toParsedFiles(graph, root);
|
|
@@ -14159,7 +14171,7 @@ function subsystemView(graph, root, warn = () => {}) {
|
|
|
14159
14171
|
}
|
|
14160
14172
|
function emitDependencyLayers(graph, root, outDir, view = subsystemView(graph, root)) {
|
|
14161
14173
|
const identity = projectIdentity(root, graph);
|
|
14162
|
-
return writeJson(
|
|
14174
|
+
return writeJson(join15(outDir, "dependency-layers.json"), {
|
|
14163
14175
|
metadata: {
|
|
14164
14176
|
name: identity.name,
|
|
14165
14177
|
version: identity.version,
|
|
@@ -14198,33 +14210,14 @@ function statisticsFromCore(core, subsystems) {
|
|
|
14198
14210
|
unusedExportsCount: num("unusedExportsCount")
|
|
14199
14211
|
};
|
|
14200
14212
|
}
|
|
14201
|
-
|
|
14202
|
-
const rows = [
|
|
14203
|
-
["Total Source Files", stats.totalTypeScriptFiles],
|
|
14204
|
-
["Subsystems", stats.totalModules],
|
|
14205
|
-
["Total Lines of Code", stats.totalLinesOfCode],
|
|
14206
|
-
["Total Exports", stats.totalExports],
|
|
14207
|
-
["Total Re-exports", stats.totalReExports],
|
|
14208
|
-
["Total Classes", stats.totalClasses],
|
|
14209
|
-
["Total Interfaces", stats.totalInterfaces],
|
|
14210
|
-
["Total Functions", stats.totalFunctions],
|
|
14211
|
-
["Total Type Guards", stats.totalTypeGuards],
|
|
14212
|
-
["Total Enums", stats.totalEnums],
|
|
14213
|
-
["Type-only Imports", stats.totalTypeOnlyImports],
|
|
14214
|
-
["Runtime Cyclic Components", stats.runtimeCyclicComponents],
|
|
14215
|
-
["Type-only Cyclic Components", stats.typeOnlyCyclicComponents],
|
|
14216
|
-
["Files in Runtime Cycles", stats.runtimeFilesInCycles],
|
|
14217
|
-
["Files in Type-only Cycles", stats.typeOnlyFilesInCycles]
|
|
14218
|
-
];
|
|
14219
|
-
return rows.filter((r) => r[1] !== undefined);
|
|
14220
|
-
}
|
|
14213
|
+
var SUMMARY_LABELS = { files: "Total Source Files", modules: "Subsystems" };
|
|
14221
14214
|
function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
|
|
14222
14215
|
const view = options.view ?? subsystemView(graph, root);
|
|
14223
14216
|
const core = JSON.parse(readFileSync12(corePath, "utf8"));
|
|
14224
14217
|
const stats = statisticsFromCore(core.statistics, Object.keys(view.modules).length);
|
|
14225
14218
|
const identity = projectIdentity(root, graph);
|
|
14226
14219
|
const banner = { command: MAP_REGENERATE_COMMAND, ...options.banner };
|
|
14227
|
-
const markdown = generateMarkdown(view.records, view.modules, stats, view.cycles, buildDependencyMatrix(view.records), identity,
|
|
14220
|
+
const markdown = generateMarkdown(view.records, view.modules, stats, view.cycles, buildDependencyMatrix(view.records), identity, summaryRowsOf(stats, SUMMARY_LABELS));
|
|
14228
14221
|
const written = [
|
|
14229
14222
|
["DEPENDENCY_GRAPH.md", withBanner(markdown, banner)],
|
|
14230
14223
|
["dependency-graph.yaml", generateYaml(core)],
|
|
@@ -14234,7 +14227,7 @@ function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
|
|
|
14234
14227
|
]
|
|
14235
14228
|
];
|
|
14236
14229
|
return written.map(([name2, text]) => {
|
|
14237
|
-
const path =
|
|
14230
|
+
const path = join16(outDir, name2);
|
|
14238
14231
|
writeReport(path, text);
|
|
14239
14232
|
return path;
|
|
14240
14233
|
});
|
|
@@ -14252,9 +14245,9 @@ function isTestFile(path, language, area) {
|
|
|
14252
14245
|
function emitTestCoverage(graph, root, outDir, options = {}) {
|
|
14253
14246
|
const sources = toParsedFiles(graph, root);
|
|
14254
14247
|
const tests = toParsedFiles(graph, root, { allAreas: true, loadEdges: true }).filter((r) => isTestFile(r.path, graph.language, graph.files.get(r.path)?.area ?? ""));
|
|
14255
|
-
const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ??
|
|
14256
|
-
const mdPath =
|
|
14257
|
-
const jsonPath =
|
|
14248
|
+
const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ?? join17(root, "docs", "architecture", "coverage-policy.json"));
|
|
14249
|
+
const mdPath = join17(outDir, "TEST_COVERAGE.md");
|
|
14250
|
+
const jsonPath = join17(outDir, "test-coverage.json");
|
|
14258
14251
|
const namespaceImportBasis = !importsNameSymbols(graph.language);
|
|
14259
14252
|
const markdown = generateTestCoverageMarkdown(coverage, {
|
|
14260
14253
|
expectedTestHint: graph.language === "typescript" && tests.some((t) => t.path.startsWith("tests/unit/")),
|
|
@@ -14268,7 +14261,7 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
|
|
|
14268
14261
|
|
|
14269
14262
|
// src/map/markdown.ts
|
|
14270
14263
|
import { readFileSync as readFileSync13 } from "node:fs";
|
|
14271
|
-
import { join as
|
|
14264
|
+
import { join as join18 } from "node:path";
|
|
14272
14265
|
|
|
14273
14266
|
// src/depgraph/reporters/duplicates.ts
|
|
14274
14267
|
function summaryTable(byTag, total) {
|
|
@@ -14308,7 +14301,7 @@ function renderTaggedSection(title, entries, tag) {
|
|
|
14308
14301
|
|
|
14309
14302
|
${renderTable(entries.filter((e) => e.tag === tag))}`;
|
|
14310
14303
|
}
|
|
14311
|
-
function generateDuplicateSymbolsMarkdown(report) {
|
|
14304
|
+
function generateDuplicateSymbolsMarkdown(report, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
14312
14305
|
let md = `# Duplicate Symbols
|
|
14313
14306
|
|
|
14314
14307
|
`;
|
|
@@ -14316,9 +14309,14 @@ function generateDuplicateSymbolsMarkdown(report) {
|
|
|
14316
14309
|
md += "A file that only re-exports the name does not count. ";
|
|
14317
14310
|
md += "Each name carries one tag (see `DupEntryTag`), so the actionable subset is clear:\n\n";
|
|
14318
14311
|
md += "- `TRUE_DUPLICATE`: a real merge target.\n";
|
|
14319
|
-
|
|
14320
|
-
|
|
14312
|
+
if (dispatchCalls.length === 0) {
|
|
14313
|
+
md += "- `DISPATCH_VARIANT`: never set, because no dispatch call is configured.\n";
|
|
14314
|
+
} else {
|
|
14315
|
+
md += `- \`DISPATCH_VARIANT\`: two or more ${describeDispatchCalls(dispatchCalls, true)} `;
|
|
14316
|
+
md += "registrations of the same public name. ";
|
|
14317
|
+
md += `The registrations are distinct dispatch surfaces, not copy-paste bodies.
|
|
14321
14318
|
`;
|
|
14319
|
+
}
|
|
14322
14320
|
md += "- `ALIAS_DELEGATION`: a `const X = importedY` forward. ";
|
|
14323
14321
|
md += `The entry is excluded once fewer than two real bodies remain.
|
|
14324
14322
|
`;
|
|
@@ -14413,7 +14411,7 @@ function inventoryMarkdown(inv, language) {
|
|
|
14413
14411
|
return lines.join(`
|
|
14414
14412
|
`);
|
|
14415
14413
|
}
|
|
14416
|
-
function duplicatesMarkdown(dup) {
|
|
14414
|
+
function duplicatesMarkdown(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
14417
14415
|
if (Array.isArray(dup.runtime) && Array.isArray(dup.types)) {
|
|
14418
14416
|
const report = {
|
|
14419
14417
|
note: dup.classificationNote,
|
|
@@ -14426,7 +14424,7 @@ function duplicatesMarkdown(dup) {
|
|
|
14426
14424
|
runtime: dup.runtime,
|
|
14427
14425
|
types: dup.types
|
|
14428
14426
|
};
|
|
14429
|
-
return generateDuplicateSymbolsMarkdown(report);
|
|
14427
|
+
return generateDuplicateSymbolsMarkdown(report, dispatchCalls);
|
|
14430
14428
|
}
|
|
14431
14429
|
const lines = ["# Duplicate Symbols", ""];
|
|
14432
14430
|
lines.push(`> **Note:** ${dup.note}`, "", `> ${dup.classificationNote}`, "");
|
|
@@ -14497,17 +14495,17 @@ function unusedMarkdown(unused, graph) {
|
|
|
14497
14495
|
return lines.join(`
|
|
14498
14496
|
`);
|
|
14499
14497
|
}
|
|
14500
|
-
function emitMarkdownReports(outDir, banner = {}) {
|
|
14501
|
-
const read = (name2) => JSON.parse(readFileSync13(
|
|
14498
|
+
function emitMarkdownReports(outDir, banner = {}, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
14499
|
+
const read = (name2) => JSON.parse(readFileSync13(join18(outDir, name2), "utf8"));
|
|
14502
14500
|
const graph = read("dependency-graph.json");
|
|
14503
14501
|
const bannerOptions = { command: MAP_REGENERATE_COMMAND, ...banner };
|
|
14504
14502
|
const reports = [
|
|
14505
14503
|
["FILE_INVENTORY.md", inventoryMarkdown(read("file-inventory.json"), graph.metadata.language)],
|
|
14506
|
-
["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"))],
|
|
14504
|
+
["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"), dispatchCalls)],
|
|
14507
14505
|
["unused-analysis.md", unusedMarkdown(read("unused-analysis.json"), graph)]
|
|
14508
14506
|
];
|
|
14509
14507
|
return reports.map(([name2, body2]) => {
|
|
14510
|
-
const path =
|
|
14508
|
+
const path = join18(outDir, name2);
|
|
14511
14509
|
writeReport(path, withBanner(body2, bannerOptions));
|
|
14512
14510
|
return path;
|
|
14513
14511
|
});
|
|
@@ -14515,7 +14513,7 @@ function emitMarkdownReports(outDir, banner = {}) {
|
|
|
14515
14513
|
|
|
14516
14514
|
// src/map/surfaces.ts
|
|
14517
14515
|
import { readFileSync as readFileSync14 } from "node:fs";
|
|
14518
|
-
import { join as
|
|
14516
|
+
import { join as join19 } from "node:path";
|
|
14519
14517
|
|
|
14520
14518
|
// src/depgraph/reporters/surfaces.ts
|
|
14521
14519
|
function packageKeyOf(moduleKey) {
|
|
@@ -14550,7 +14548,7 @@ function emitApiSurface(graph, root, outPath, entry, stabilityTags) {
|
|
|
14550
14548
|
if (graph.language === "typescript") {
|
|
14551
14549
|
const load = (p) => {
|
|
14552
14550
|
try {
|
|
14553
|
-
return readFileSync14(
|
|
14551
|
+
return readFileSync14(join19(root, p), "utf8");
|
|
14554
14552
|
} catch {
|
|
14555
14553
|
return null;
|
|
14556
14554
|
}
|
|
@@ -14596,7 +14594,7 @@ function sortedSurfaces(map) {
|
|
|
14596
14594
|
return out2;
|
|
14597
14595
|
}
|
|
14598
14596
|
function emitExportSurfaces(graph, root, outDir) {
|
|
14599
|
-
const path =
|
|
14597
|
+
const path = join19(outDir, "package-export-surfaces.json");
|
|
14600
14598
|
if (graph.language === "typescript") {
|
|
14601
14599
|
const records = toParsedFiles(graph, root);
|
|
14602
14600
|
const workspaces = detectWorkspaces(root);
|
|
@@ -14724,7 +14722,7 @@ async function runMap(argv, io, command = "map") {
|
|
|
14724
14722
|
`);
|
|
14725
14723
|
if (options.strictOrphans)
|
|
14726
14724
|
options.settings.strictOrphans = true;
|
|
14727
|
-
root =
|
|
14725
|
+
root = resolve5(options.root);
|
|
14728
14726
|
if (!isDirectory(root))
|
|
14729
14727
|
throw new Error("the root <root> is not an existing directory");
|
|
14730
14728
|
const fileSettings = loadConfigFile(root, options.config);
|
|
@@ -14791,13 +14789,14 @@ async function runPipeline(root, options, config, io) {
|
|
|
14791
14789
|
const corePath = emitDependencyGraph(graph, outDir);
|
|
14792
14790
|
written.push(corePath, emitFileInventory(graph, outDir));
|
|
14793
14791
|
written.push(emitDuplicateSymbols(graph, outDir, {
|
|
14794
|
-
allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist)
|
|
14792
|
+
allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist),
|
|
14793
|
+
dispatchCalls: config.dispatchCalls
|
|
14795
14794
|
}));
|
|
14796
14795
|
written.push(emitUnusedAnalysis(graph, outDir));
|
|
14797
14796
|
const view = subsystemView(graph, root);
|
|
14798
14797
|
written.push(emitDependencyLayers(graph, root, outDir, view));
|
|
14799
14798
|
written.push(...emitSubsystemReports(graph, root, outDir, corePath, { view, banner }));
|
|
14800
|
-
written.push(...emitMarkdownReports(outDir, banner));
|
|
14799
|
+
written.push(...emitMarkdownReports(outDir, banner, config.dispatchCalls));
|
|
14801
14800
|
written.push(...emitTestCoverage(graph, root, outDir, {
|
|
14802
14801
|
policyPath: resolveUnderRoot(root, config.coveragePolicy),
|
|
14803
14802
|
policyNote: config.coveragePolicy.replace(/\\/g, "/"),
|
|
@@ -15001,7 +15000,7 @@ function parseClaims(markdown) {
|
|
|
15001
15000
|
// src/check/metrics.ts
|
|
15002
15001
|
import { mkdtempSync, readFileSync as readFileSync16, rmSync } from "node:fs";
|
|
15003
15002
|
import { tmpdir } from "node:os";
|
|
15004
|
-
import { join as
|
|
15003
|
+
import { join as join20 } from "node:path";
|
|
15005
15004
|
var readReport = (path) => JSON.parse(readFileSync16(path, "utf8"));
|
|
15006
15005
|
function section(report, key) {
|
|
15007
15006
|
const value = report[key];
|
|
@@ -15019,11 +15018,14 @@ function addScalars(into, source) {
|
|
|
15019
15018
|
}
|
|
15020
15019
|
async function collectMetrics(root, options = {}) {
|
|
15021
15020
|
const graph = await graphOf(root);
|
|
15022
|
-
const scratch = mkdtempSync(
|
|
15021
|
+
const scratch = mkdtempSync(join20(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
|
|
15023
15022
|
try {
|
|
15024
15023
|
const dep = readReport(emitDependencyGraph(graph, scratch));
|
|
15025
15024
|
const inventory = readReport(emitFileInventory(graph, scratch));
|
|
15026
|
-
const duplicates = readReport(emitDuplicateSymbols(graph, scratch,
|
|
15025
|
+
const duplicates = readReport(emitDuplicateSymbols(graph, scratch, {
|
|
15026
|
+
...options.allowlistPath === undefined ? {} : { allowlistPath: options.allowlistPath },
|
|
15027
|
+
...options.dispatchCalls === undefined ? {} : { dispatchCalls: options.dispatchCalls }
|
|
15028
|
+
}));
|
|
15027
15029
|
const unused = readReport(emitUnusedAnalysis(graph, scratch));
|
|
15028
15030
|
const metrics = new Map;
|
|
15029
15031
|
addScalars(metrics, section(dep, "metadata"));
|
|
@@ -15221,7 +15223,7 @@ function readDocs(dir, label) {
|
|
|
15221
15223
|
return names.map((name2) => {
|
|
15222
15224
|
let bytes;
|
|
15223
15225
|
try {
|
|
15224
|
-
bytes = readFileSync17(
|
|
15226
|
+
bytes = readFileSync17(join21(dir, name2));
|
|
15225
15227
|
} catch {
|
|
15226
15228
|
throw new Error(`cannot read ${label}/${name2}`);
|
|
15227
15229
|
}
|
|
@@ -15248,16 +15250,17 @@ async function run2(argv, io) {
|
|
|
15248
15250
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
15249
15251
|
try {
|
|
15250
15252
|
const options = parseCheckArgs(argv, process.cwd());
|
|
15251
|
-
root =
|
|
15253
|
+
root = resolve6(options.root);
|
|
15252
15254
|
if (!isDirectory(root))
|
|
15253
15255
|
throw new Error("the root <root> is not an existing directory");
|
|
15254
15256
|
if (options.docs === undefined)
|
|
15255
15257
|
throw new Error("flag --docs is required: --docs=<dir>");
|
|
15256
15258
|
const config = mergeMapConfig({}, loadConfigFile(root, options.config));
|
|
15257
15259
|
const label = isAbsolutePath(options.docs) ? "<docs>" : `<root>/${options.docs.replace(/\\/g, "/")}`;
|
|
15258
|
-
const docs = readDocs(
|
|
15260
|
+
const docs = readDocs(resolve6(root, options.docs), label);
|
|
15259
15261
|
const measured = await collectMetrics(root, {
|
|
15260
|
-
allowlistPath:
|
|
15262
|
+
allowlistPath: resolve6(root, config.duplicateAllowlist),
|
|
15263
|
+
dispatchCalls: config.dispatchCalls
|
|
15261
15264
|
});
|
|
15262
15265
|
const markers = [NO_VERIFICATION_MARKER];
|
|
15263
15266
|
if (config.verificationMarker !== null && !markers.includes(config.verificationMarker)) {
|
|
@@ -15289,12 +15292,12 @@ import {
|
|
|
15289
15292
|
statSync as statSync7,
|
|
15290
15293
|
writeFileSync as writeFileSync3
|
|
15291
15294
|
} from "node:fs";
|
|
15292
|
-
import { basename as basename7, dirname as
|
|
15295
|
+
import { basename as basename7, dirname as dirname6, extname as extname2, isAbsolute as isAbsolute3, join as join22, relative as relative4, resolve as resolve8, sep as sep3 } from "node:path";
|
|
15293
15296
|
|
|
15294
15297
|
// src/chunk/manifest.ts
|
|
15295
15298
|
import { createHash } from "node:crypto";
|
|
15296
15299
|
import { readFileSync as readFileSync18 } from "node:fs";
|
|
15297
|
-
import { posix as posix3, relative as relative3, resolve as
|
|
15300
|
+
import { posix as posix3, relative as relative3, resolve as resolve7, sep as sep2, win32 as win322 } from "node:path";
|
|
15298
15301
|
|
|
15299
15302
|
// src/chunk/splitters.ts
|
|
15300
15303
|
import { extname } from "node:path";
|
|
@@ -15925,7 +15928,7 @@ function relativeSource(manifestDir, sourcePath) {
|
|
|
15925
15928
|
return relative3(manifestDir, sourcePath).split(sep2).join("/");
|
|
15926
15929
|
}
|
|
15927
15930
|
function resolveSource(manifestDir, sourceFile) {
|
|
15928
|
-
return
|
|
15931
|
+
return resolve7(manifestDir, sourceFile);
|
|
15929
15932
|
}
|
|
15930
15933
|
function readManifest(path) {
|
|
15931
15934
|
return validateManifest(JSON.parse(readFileSync18(path, "utf8")));
|
|
@@ -16144,34 +16147,18 @@ function isOutsideText(folder, path) {
|
|
|
16144
16147
|
const rel = relative4(folder, path);
|
|
16145
16148
|
return rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel);
|
|
16146
16149
|
}
|
|
16147
|
-
function realPath2(path) {
|
|
16148
|
-
let existing = resolve7(path);
|
|
16149
|
-
const rest = [];
|
|
16150
|
-
while (!lstatSync4(existing, { throwIfNoEntry: false })) {
|
|
16151
|
-
const up = dirname7(existing);
|
|
16152
|
-
if (up === existing)
|
|
16153
|
-
return;
|
|
16154
|
-
rest.unshift(basename7(existing));
|
|
16155
|
-
existing = up;
|
|
16156
|
-
}
|
|
16157
|
-
try {
|
|
16158
|
-
return join21(realpathSync2.native(existing), ...rest);
|
|
16159
|
-
} catch {
|
|
16160
|
-
return;
|
|
16161
|
-
}
|
|
16162
|
-
}
|
|
16163
16150
|
function isOutside(folder, path) {
|
|
16164
16151
|
if (isOutsideText(folder, path))
|
|
16165
16152
|
return true;
|
|
16166
|
-
const realFolder =
|
|
16167
|
-
const realTarget =
|
|
16153
|
+
const realFolder = realPath(folder);
|
|
16154
|
+
const realTarget = realPath(path);
|
|
16168
16155
|
return realFolder === undefined || realTarget === undefined || isOutsideText(realFolder, realTarget);
|
|
16169
16156
|
}
|
|
16170
16157
|
function isInside(folder, path) {
|
|
16171
16158
|
if (!isOutsideText(folder, path))
|
|
16172
16159
|
return true;
|
|
16173
|
-
const realFolder =
|
|
16174
|
-
const realTarget =
|
|
16160
|
+
const realFolder = realPath(folder);
|
|
16161
|
+
const realTarget = realPath(path);
|
|
16175
16162
|
return realFolder !== undefined && realTarget !== undefined && !isOutsideText(realFolder, realTarget);
|
|
16176
16163
|
}
|
|
16177
16164
|
function chunkFolderError(chunksDir, paths) {
|
|
@@ -16184,11 +16171,11 @@ function chunkFolderError(chunksDir, paths) {
|
|
|
16184
16171
|
return;
|
|
16185
16172
|
}
|
|
16186
16173
|
function outsideError(sourcePath, chunksDir, remedy) {
|
|
16187
|
-
return `Error: the source file ${sourcePath} is outside ${
|
|
16174
|
+
return `Error: the source file ${sourcePath} is outside ${dirname6(chunksDir)}, the parent folder of the chunk folder. ${remedy}
|
|
16188
16175
|
`;
|
|
16189
16176
|
}
|
|
16190
16177
|
function readChunk(chunksDir, filename) {
|
|
16191
|
-
const chunkPath =
|
|
16178
|
+
const chunkPath = join22(chunksDir, filename);
|
|
16192
16179
|
const stat = lstatSync4(chunkPath, { throwIfNoEntry: false });
|
|
16193
16180
|
if (!stat)
|
|
16194
16181
|
return;
|
|
@@ -16222,7 +16209,7 @@ function split(inputFile, options, io) {
|
|
|
16222
16209
|
const log = lineWriter(io);
|
|
16223
16210
|
const maxLines = options.maxLines ?? 500;
|
|
16224
16211
|
const dryRun = options.dryRun ?? false;
|
|
16225
|
-
const absoluteInput =
|
|
16212
|
+
const absoluteInput = resolve8(inputFile);
|
|
16226
16213
|
if (!existsSync8(absoluteInput)) {
|
|
16227
16214
|
io.stderr(`Error: File not found: ${absoluteInput}
|
|
16228
16215
|
`);
|
|
@@ -16239,7 +16226,7 @@ function split(inputFile, options, io) {
|
|
|
16239
16226
|
const content = readFileSync19(absoluteInput, "utf8");
|
|
16240
16227
|
const sourceHash = contentHash(content);
|
|
16241
16228
|
const baseName = basename7(inputFile, extname2(inputFile));
|
|
16242
|
-
const outputDir = options.output ?
|
|
16229
|
+
const outputDir = options.output ? resolve8(options.output) : join22(dirname6(absoluteInput), `${baseName}_chunks`);
|
|
16243
16230
|
const sourceFile = relativeSource(outputDir, absoluteInput);
|
|
16244
16231
|
if (isAbsoluteAnywhere(sourceFile)) {
|
|
16245
16232
|
io.stderr(`Error: the chunk folder ${outputDir} is on another volume than ${absoluteInput}. Nothing was written. Put the chunk folder on the same volume as the source file.
|
|
@@ -16281,7 +16268,7 @@ chunker - Splitting File`);
|
|
|
16281
16268
|
const large = lineCount > maxLines ? " ⚠️ [LARGE]" : "";
|
|
16282
16269
|
log(` ${String(i2 + 1).padStart(2)}. [${levelIndicator(fileType, section.level).padEnd(4)}] ${section.title.substring(0, 40).padEnd(40)} ${String(lineCount).padStart(4)} lines${large}`);
|
|
16283
16270
|
if (!dryRun)
|
|
16284
|
-
writeLf(
|
|
16271
|
+
writeLf(join22(outputDir, filename), section.content);
|
|
16285
16272
|
return {
|
|
16286
16273
|
index: i2 + 1,
|
|
16287
16274
|
filename,
|
|
@@ -16305,7 +16292,7 @@ chunker - Splitting File`);
|
|
|
16305
16292
|
...lineEnding === "crlf" ? { lineEnding } : {},
|
|
16306
16293
|
...lineEnding === "mixed" ? { lineBreaks: lineBreakRuns(content) } : {}
|
|
16307
16294
|
};
|
|
16308
|
-
const manifestPath =
|
|
16295
|
+
const manifestPath = join22(outputDir, MANIFEST_NAME);
|
|
16309
16296
|
if (!dryRun) {
|
|
16310
16297
|
writeManifest(manifestPath, manifest);
|
|
16311
16298
|
log(`
|
|
@@ -16334,7 +16321,7 @@ To edit: Modify individual chunk files in the directory`);
|
|
|
16334
16321
|
function merge(manifestFile, options, io) {
|
|
16335
16322
|
const log = lineWriter(io);
|
|
16336
16323
|
const dryRun = options.dryRun ?? false;
|
|
16337
|
-
const absoluteManifest =
|
|
16324
|
+
const absoluteManifest = resolve8(manifestFile);
|
|
16338
16325
|
if (!existsSync8(absoluteManifest)) {
|
|
16339
16326
|
io.stderr(`Error: Manifest not found: ${absoluteManifest}
|
|
16340
16327
|
`);
|
|
@@ -16347,17 +16334,17 @@ function merge(manifestFile, options, io) {
|
|
|
16347
16334
|
return 1;
|
|
16348
16335
|
}
|
|
16349
16336
|
const manifest = readManifest(absoluteManifest);
|
|
16350
|
-
const chunksDir =
|
|
16337
|
+
const chunksDir = dirname6(absoluteManifest);
|
|
16351
16338
|
const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
|
|
16352
16339
|
const selfError = chunkFolderError(chunksDir, {
|
|
16353
16340
|
"source file": sourcePath,
|
|
16354
|
-
...options.output ? { "output file":
|
|
16341
|
+
...options.output ? { "output file": resolve8(options.output) } : {}
|
|
16355
16342
|
});
|
|
16356
16343
|
if (selfError) {
|
|
16357
16344
|
io.stderr(selfError);
|
|
16358
16345
|
return 1;
|
|
16359
16346
|
}
|
|
16360
|
-
const outside = isOutside(
|
|
16347
|
+
const outside = isOutside(dirname6(chunksDir), sourcePath);
|
|
16361
16348
|
if (outside && !options.output && !options.yes) {
|
|
16362
16349
|
io.stderr(outsideError(sourcePath, chunksDir, "Use -o <file> to name the target, or --yes to confirm."));
|
|
16363
16350
|
return 1;
|
|
@@ -16380,7 +16367,7 @@ chunker - Merging Chunks`);
|
|
|
16380
16367
|
for (const chunk of manifest.chunks) {
|
|
16381
16368
|
const content = readChunk(chunksDir, chunk.filename);
|
|
16382
16369
|
if (content === undefined) {
|
|
16383
|
-
io.stderr(`Error: Missing chunk file: ${
|
|
16370
|
+
io.stderr(`Error: Missing chunk file: ${join22(chunksDir, chunk.filename)}
|
|
16384
16371
|
`);
|
|
16385
16372
|
return 1;
|
|
16386
16373
|
}
|
|
@@ -16413,7 +16400,7 @@ Modified chunks: ${modifiedCount} of ${manifest.chunks.length}`);
|
|
|
16413
16400
|
NOTE: the number of lines changed. All line breaks use the most common original one.`);
|
|
16414
16401
|
}
|
|
16415
16402
|
}
|
|
16416
|
-
const outputPath = options.output ?
|
|
16403
|
+
const outputPath = options.output ? resolve8(options.output) : sourcePath;
|
|
16417
16404
|
if (readSource && existsSync8(sourcePath)) {
|
|
16418
16405
|
const currentSourceHash = hash(readFileSync19(sourcePath, "utf8"));
|
|
16419
16406
|
if (currentSourceHash !== manifest.sourceHash) {
|
|
@@ -16454,7 +16441,7 @@ Merged file written: ${outputPath}`);
|
|
|
16454
16441
|
}
|
|
16455
16442
|
function status(manifestFile, options, io) {
|
|
16456
16443
|
const log = lineWriter(io);
|
|
16457
|
-
const absoluteManifest =
|
|
16444
|
+
const absoluteManifest = resolve8(manifestFile);
|
|
16458
16445
|
if (!existsSync8(absoluteManifest)) {
|
|
16459
16446
|
io.stderr(`Error: Manifest not found: ${absoluteManifest}
|
|
16460
16447
|
`);
|
|
@@ -16467,14 +16454,14 @@ function status(manifestFile, options, io) {
|
|
|
16467
16454
|
return 1;
|
|
16468
16455
|
}
|
|
16469
16456
|
const manifest = readManifest(absoluteManifest);
|
|
16470
|
-
const chunksDir =
|
|
16457
|
+
const chunksDir = dirname6(absoluteManifest);
|
|
16471
16458
|
const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
|
|
16472
16459
|
const selfError = chunkFolderError(chunksDir, { "source file": sourcePath });
|
|
16473
16460
|
if (selfError) {
|
|
16474
16461
|
io.stderr(selfError);
|
|
16475
16462
|
return 1;
|
|
16476
16463
|
}
|
|
16477
|
-
if (isOutside(
|
|
16464
|
+
if (isOutside(dirname6(chunksDir), sourcePath) && !options.yes) {
|
|
16478
16465
|
io.stderr(outsideError(sourcePath, chunksDir, "Use --yes to read it."));
|
|
16479
16466
|
return 1;
|
|
16480
16467
|
}
|
|
@@ -16595,7 +16582,7 @@ ${CHUNK_HELP}`);
|
|
|
16595
16582
|
}
|
|
16596
16583
|
|
|
16597
16584
|
// src/clones/index.ts
|
|
16598
|
-
import { join as
|
|
16585
|
+
import { join as join23, resolve as resolve9 } from "node:path";
|
|
16599
16586
|
|
|
16600
16587
|
// src/clones/detect.ts
|
|
16601
16588
|
function fnv1a(text) {
|
|
@@ -16729,7 +16716,7 @@ ${items.map((item) => ` ${JSON.stringify(item)}`).join(`,
|
|
|
16729
16716
|
]`;
|
|
16730
16717
|
}
|
|
16731
16718
|
function clonesJson(config, detection) {
|
|
16732
|
-
const { out: _out, ...params } = config;
|
|
16719
|
+
const { out: _out, parseErrorList: _list, ...params } = config;
|
|
16733
16720
|
return [
|
|
16734
16721
|
"{",
|
|
16735
16722
|
` "params": ${JSON.stringify(params)},`,
|
|
@@ -16765,8 +16752,15 @@ function summaryText(config, stats, units, detection) {
|
|
|
16765
16752
|
byFolder.set(key, (byFolder.get(key) ?? 0) + 1);
|
|
16766
16753
|
}
|
|
16767
16754
|
const folders = [...byFolder.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, count]) => `${key}=${count}`).join(" ");
|
|
16755
|
+
const failed = stats.parseErrorFiles.length;
|
|
16756
|
+
const paths = [...stats.parseErrorFiles].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, config.parseErrorList);
|
|
16757
|
+
const errorList = failed === 0 ? [] : [
|
|
16758
|
+
`files with a parse error (${paths.length < failed ? "first " : ""}${paths.length} of ${failed}):`,
|
|
16759
|
+
...paths.map((path) => ` ${path}`)
|
|
16760
|
+
];
|
|
16768
16761
|
const lines = [
|
|
16769
|
-
`files=${stats.files} parseErrors=${
|
|
16762
|
+
`files=${stats.files} parseErrors=${failed} units(minNodes=${config.minNodes})=${units.length}`,
|
|
16763
|
+
...errorList,
|
|
16770
16764
|
`nodes p50=${percentile(sizes, 0.5)} p90=${percentile(sizes, 0.9)} p99=${percentile(sizes, 0.99)} max=${sizes[sizes.length - 1] ?? 0}`,
|
|
16771
16765
|
`exact groups=${detection.exactGroups.length} (units in them=${inGroups}) text-identical groups=${detection.textGroups.length}`,
|
|
16772
16766
|
`near pairs=${detection.near.length} by jStruct: ${bands.join(", ")}`,
|
|
@@ -16787,7 +16781,8 @@ var CLONES_DEFAULTS = Object.freeze({
|
|
|
16787
16781
|
containmentThreshold: 0.9,
|
|
16788
16782
|
containmentMinShingles: 20,
|
|
16789
16783
|
rareShingleLimit: 60,
|
|
16790
|
-
minSharedShingles: 3
|
|
16784
|
+
minSharedShingles: 3,
|
|
16785
|
+
parseErrorList: 20
|
|
16791
16786
|
});
|
|
16792
16787
|
function mergeClonesConfig(cli, file) {
|
|
16793
16788
|
const d = CLONES_DEFAULTS;
|
|
@@ -16801,7 +16796,8 @@ function mergeClonesConfig(cli, file) {
|
|
|
16801
16796
|
containmentThreshold: cli.containmentThreshold ?? file.containmentThreshold ?? d.containmentThreshold,
|
|
16802
16797
|
containmentMinShingles: cli.containmentMinShingles ?? file.containmentMinShingles ?? d.containmentMinShingles,
|
|
16803
16798
|
rareShingleLimit: cli.rareShingleLimit ?? file.rareShingleLimit ?? d.rareShingleLimit,
|
|
16804
|
-
minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles
|
|
16799
|
+
minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles,
|
|
16800
|
+
parseErrorList: cli.parseErrorList ?? file.parseErrorList ?? d.parseErrorList
|
|
16805
16801
|
};
|
|
16806
16802
|
}
|
|
16807
16803
|
|
|
@@ -17046,7 +17042,7 @@ function unitsOf(root, source, file, minNodes) {
|
|
|
17046
17042
|
var CLONES_HELP = `Usage: repo-tools clones [root] [--out=<dir>] [--config=<path>] [--src=<path>...]
|
|
17047
17043
|
[--min-nodes=<n>] [--shingle=<n>] [--near=<f>]
|
|
17048
17044
|
[--containment=<f>] [--containment-min=<n>]
|
|
17049
|
-
[--rare=<n>] [--min-shared=<n>]
|
|
17045
|
+
[--rare=<n>] [--min-shared=<n>] [--list-errors=<n>]
|
|
17050
17046
|
|
|
17051
17047
|
List the functions of a TypeScript or JavaScript repository and find the functions
|
|
17052
17048
|
that copy each other. The command only reads the repository. The result is a list
|
|
@@ -17074,6 +17070,8 @@ shingles (runs of --shingle labels) overlap enough are a near pair.
|
|
|
17074
17070
|
pairs. Default 60.
|
|
17075
17071
|
--min-shared=<n> The rare shingles that a candidate pair must share.
|
|
17076
17072
|
Default 3.
|
|
17073
|
+
--list-errors=<n> The most paths of files with a parse error that the
|
|
17074
|
+
summary lists. Default 20.
|
|
17077
17075
|
--root=<path> The root. The default is the current folder.
|
|
17078
17076
|
--help, -h Show this help.
|
|
17079
17077
|
|
|
@@ -17093,7 +17091,8 @@ var VALUE_FLAGS2 = {
|
|
|
17093
17091
|
"--containment": "containmentThreshold",
|
|
17094
17092
|
"--containment-min": "containmentMinShingles",
|
|
17095
17093
|
"--rare": "rareShingleLimit",
|
|
17096
|
-
"--min-shared": "minSharedShingles"
|
|
17094
|
+
"--min-shared": "minSharedShingles",
|
|
17095
|
+
"--list-errors": "parseErrorList"
|
|
17097
17096
|
};
|
|
17098
17097
|
var NUMERIC = new Set([
|
|
17099
17098
|
"minNodes",
|
|
@@ -17102,7 +17101,8 @@ var NUMERIC = new Set([
|
|
|
17102
17101
|
"containmentThreshold",
|
|
17103
17102
|
"containmentMinShingles",
|
|
17104
17103
|
"rareShingleLimit",
|
|
17105
|
-
"minSharedShingles"
|
|
17104
|
+
"minSharedShingles",
|
|
17105
|
+
"parseErrorList"
|
|
17106
17106
|
]);
|
|
17107
17107
|
function parseArgs2(argv) {
|
|
17108
17108
|
const options = { root: ".", config: undefined, settings: {} };
|
|
@@ -17179,39 +17179,39 @@ function selectFiles(root, src) {
|
|
|
17179
17179
|
}
|
|
17180
17180
|
async function readUnits(root, files, minNodes) {
|
|
17181
17181
|
const units = [];
|
|
17182
|
-
|
|
17182
|
+
const parseErrorFiles = [];
|
|
17183
17183
|
for (const file of files) {
|
|
17184
|
-
const source = readSource(
|
|
17184
|
+
const source = readSource(join23(root, file)).replace(/^\uFEFF/, "");
|
|
17185
17185
|
const grammar = grammarFor(file);
|
|
17186
17186
|
await loadGrammar(grammar);
|
|
17187
17187
|
const tree = parserFor(grammar).parse(source);
|
|
17188
17188
|
if (!tree) {
|
|
17189
|
-
|
|
17189
|
+
parseErrorFiles.push(file);
|
|
17190
17190
|
continue;
|
|
17191
17191
|
}
|
|
17192
17192
|
try {
|
|
17193
17193
|
if (tree.rootNode.hasError)
|
|
17194
|
-
|
|
17194
|
+
parseErrorFiles.push(file);
|
|
17195
17195
|
units.push(...unitsOf(tree.rootNode, source, file, minNodes));
|
|
17196
17196
|
} finally {
|
|
17197
17197
|
tree.delete();
|
|
17198
17198
|
}
|
|
17199
17199
|
}
|
|
17200
|
-
return { units,
|
|
17200
|
+
return { units, parseErrorFiles };
|
|
17201
17201
|
}
|
|
17202
17202
|
async function execute(root, config, io) {
|
|
17203
17203
|
const files = selectFiles(root, config.src);
|
|
17204
17204
|
if (files.length === 0) {
|
|
17205
17205
|
throw new NothingToRead(config.src === "auto" ? "no TypeScript or JavaScript file in the src area (set clones.src or pass --src=<path>)" : "no TypeScript or JavaScript file below the listed folders");
|
|
17206
17206
|
}
|
|
17207
|
-
const { units,
|
|
17207
|
+
const { units, parseErrorFiles } = await readUnits(root, files, config.minNodes);
|
|
17208
17208
|
const detection = detect(units, config);
|
|
17209
|
-
io.stdout(summaryText(config, { files: files.length,
|
|
17209
|
+
io.stdout(summaryText(config, { files: files.length, parseErrorFiles }, units, detection));
|
|
17210
17210
|
if (config.out !== null) {
|
|
17211
|
-
const out2 =
|
|
17211
|
+
const out2 = resolve9(root, config.out);
|
|
17212
17212
|
try {
|
|
17213
|
-
writeLf(
|
|
17214
|
-
writeLf(
|
|
17213
|
+
writeLf(join23(out2, "inventory.jsonl"), inventoryJsonl(units));
|
|
17214
|
+
writeLf(join23(out2, "clones.json"), clonesJson(config, detection));
|
|
17215
17215
|
} catch {
|
|
17216
17216
|
throw new Error(`cannot write the output folder '${config.out}'`);
|
|
17217
17217
|
}
|
|
@@ -17227,7 +17227,7 @@ async function run4(argv, io) {
|
|
|
17227
17227
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
17228
17228
|
try {
|
|
17229
17229
|
const options = parseArgs2(argv);
|
|
17230
|
-
root =
|
|
17230
|
+
root = resolve9(options.root);
|
|
17231
17231
|
if (!isDirectory(root))
|
|
17232
17232
|
throw new Error("the root <root> is not an existing directory");
|
|
17233
17233
|
const file = loadConfigSections(root, options.config).clones;
|
|
@@ -17242,7 +17242,7 @@ async function run4(argv, io) {
|
|
|
17242
17242
|
|
|
17243
17243
|
// src/compress/index.ts
|
|
17244
17244
|
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
17245
|
-
import { basename as basename8, dirname as
|
|
17245
|
+
import { basename as basename8, dirname as dirname7, extname as extname4, join as join24 } from "node:path";
|
|
17246
17246
|
|
|
17247
17247
|
// src/compress/formats.ts
|
|
17248
17248
|
import { extname as extname3 } from "node:path";
|
|
@@ -17998,7 +17998,7 @@ function findFiles(dir, pattern, recursive, deps = defaultDeps) {
|
|
|
17998
17998
|
const scan = (current) => {
|
|
17999
17999
|
const entries = [...deps.readdir(current)].sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
18000
18000
|
for (const entry of entries) {
|
|
18001
|
-
const full =
|
|
18001
|
+
const full = join24(current, entry.name);
|
|
18002
18002
|
if (entry.isDirectory()) {
|
|
18003
18003
|
if (recursive && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
18004
18004
|
scan(full);
|
|
@@ -18015,16 +18015,16 @@ function isDirectory2(path) {
|
|
|
18015
18015
|
}
|
|
18016
18016
|
function compactName(file) {
|
|
18017
18017
|
const ext = extname4(file);
|
|
18018
|
-
return
|
|
18018
|
+
return join24(dirname7(file), `${basename8(file, ext)}.compact${ext}`);
|
|
18019
18019
|
}
|
|
18020
18020
|
function restoredName(file) {
|
|
18021
18021
|
const base = basename8(file);
|
|
18022
18022
|
const stripped = base.replace(/\.compact(?=\.[^.]*$|$)/i, "");
|
|
18023
|
-
const output = stripped === base ? file :
|
|
18023
|
+
const output = stripped === base ? file : join24(dirname7(file), stripped);
|
|
18024
18024
|
if (output !== file)
|
|
18025
18025
|
return output;
|
|
18026
18026
|
const ext = extname4(file);
|
|
18027
|
-
return
|
|
18027
|
+
return join24(dirname7(file), `${basename8(file, ext)}.restored${ext}`);
|
|
18028
18028
|
}
|
|
18029
18029
|
function assertCanRestore(output, o) {
|
|
18030
18030
|
if (!o.dryRun && !o.yes && existsSync9(output)) {
|
|
@@ -18238,16 +18238,16 @@ async function run5(argv, io, deps = defaultDeps) {
|
|
|
18238
18238
|
|
|
18239
18239
|
// src/docs/index.ts
|
|
18240
18240
|
import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
18241
|
-
import { basename as basename9, isAbsolute as isAbsolute4, join as
|
|
18241
|
+
import { basename as basename9, isAbsolute as isAbsolute4, join as join28, relative as relative5, resolve as resolve10, sep as sep4 } from "node:path";
|
|
18242
18242
|
|
|
18243
18243
|
// src/docs/analyse.ts
|
|
18244
18244
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
18245
|
-
import { join as
|
|
18245
|
+
import { join as join26 } from "node:path";
|
|
18246
18246
|
|
|
18247
18247
|
// src/docs/discovery.ts
|
|
18248
18248
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18249
18249
|
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
|
|
18250
|
-
import { join as
|
|
18250
|
+
import { join as join25 } from "node:path";
|
|
18251
18251
|
var PYTHON_SUFFIXES2 = new Set([".py"]);
|
|
18252
18252
|
var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
18253
18253
|
var SKIP_DIRS2 = new Set([
|
|
@@ -18331,8 +18331,8 @@ function walkSource(root) {
|
|
|
18331
18331
|
const rel = `${prefix}${entry.name}`;
|
|
18332
18332
|
if (entry.isDirectory()) {
|
|
18333
18333
|
if (!SKIP_DIRS2.has(entry.name))
|
|
18334
|
-
go(
|
|
18335
|
-
} else if (isSource(rel) && statOk(
|
|
18334
|
+
go(join25(dir, entry.name), `${rel}/`);
|
|
18335
|
+
} else if (isSource(rel) && statOk(join25(dir, entry.name))) {
|
|
18336
18336
|
found.push(rel);
|
|
18337
18337
|
}
|
|
18338
18338
|
}
|
|
@@ -18348,7 +18348,7 @@ function statOk(path) {
|
|
|
18348
18348
|
}
|
|
18349
18349
|
}
|
|
18350
18350
|
function loadExclusions(root) {
|
|
18351
|
-
const file =
|
|
18351
|
+
const file = join25(root, CONFIG_NAME);
|
|
18352
18352
|
if (!existsSync10(file) || !statOk(file))
|
|
18353
18353
|
return [];
|
|
18354
18354
|
let data;
|
|
@@ -18956,9 +18956,9 @@ var DECLARATIONS = {
|
|
|
18956
18956
|
var DECLARATION_TYPES = Object.keys(DECLARATIONS);
|
|
18957
18957
|
var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
|
|
18958
18958
|
var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
|
|
18959
|
-
var
|
|
18959
|
+
var EXPORT_TYPE_STAR2 = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
|
|
18960
18960
|
function sanitise(source) {
|
|
18961
|
-
return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(
|
|
18961
|
+
return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR2, "$1 $2");
|
|
18962
18962
|
}
|
|
18963
18963
|
var IDENTIFIERS2 = new Set(["identifier", "shorthand_property_identifier_pattern"]);
|
|
18964
18964
|
function boundIdentifiers(node, text) {
|
|
@@ -19203,7 +19203,7 @@ async function analyseFile(root, rel) {
|
|
|
19203
19203
|
const language = languageOf(rel);
|
|
19204
19204
|
let source;
|
|
19205
19205
|
try {
|
|
19206
|
-
source = readSourceText2(
|
|
19206
|
+
source = readSourceText2(join26(root, rel));
|
|
19207
19207
|
} catch (error) {
|
|
19208
19208
|
const report = newReport(rel, language);
|
|
19209
19209
|
report.error = `unreadable: ${readFailure(error)}`;
|
|
@@ -19220,7 +19220,7 @@ async function analyseRepo(root) {
|
|
|
19220
19220
|
}
|
|
19221
19221
|
|
|
19222
19222
|
// src/docs/report.ts
|
|
19223
|
-
import { join as
|
|
19223
|
+
import { join as join27 } from "node:path";
|
|
19224
19224
|
function percent3(part, whole) {
|
|
19225
19225
|
if (whole === 0)
|
|
19226
19226
|
return 100;
|
|
@@ -19332,8 +19332,8 @@ function symbolJson(s) {
|
|
|
19332
19332
|
};
|
|
19333
19333
|
}
|
|
19334
19334
|
function writeOutputs(outDir, reports, stats, provenance, rootName) {
|
|
19335
|
-
const markdown =
|
|
19336
|
-
const json =
|
|
19335
|
+
const markdown = join27(outDir, "COVERAGE.md");
|
|
19336
|
+
const json = join27(outDir, "coverage.json");
|
|
19337
19337
|
writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
|
|
19338
19338
|
const payload = {
|
|
19339
19339
|
summary: statsJson(stats),
|
|
@@ -19685,7 +19685,7 @@ async function runScan(root, options, io) {
|
|
|
19685
19685
|
if (reports.length === 0)
|
|
19686
19686
|
throw new NothingToMeasure(provenance);
|
|
19687
19687
|
const stats = summarise(reports);
|
|
19688
|
-
const outDir =
|
|
19688
|
+
const outDir = resolve10(options.out ?? join28(root, "docs", "code-docs"));
|
|
19689
19689
|
const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
|
|
19690
19690
|
io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
|
|
19691
19691
|
`);
|
|
@@ -19725,7 +19725,7 @@ async function runStub(root, options, io) {
|
|
|
19725
19725
|
const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
|
|
19726
19726
|
if (targets.length === 0)
|
|
19727
19727
|
continue;
|
|
19728
|
-
const file =
|
|
19728
|
+
const file = join28(root, report.path);
|
|
19729
19729
|
if (leavesRoot(root, file)) {
|
|
19730
19730
|
io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
|
|
19731
19731
|
`);
|
|
@@ -19860,7 +19860,7 @@ async function run6(argv, io) {
|
|
|
19860
19860
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
19861
19861
|
try {
|
|
19862
19862
|
const options = parseDocsArgs(argv);
|
|
19863
|
-
root =
|
|
19863
|
+
root = resolve10(options.root);
|
|
19864
19864
|
if (!isDirectory(root))
|
|
19865
19865
|
throw new Error("the root <root> is not an existing directory");
|
|
19866
19866
|
assertReadableLanguage(root);
|
|
@@ -19879,7 +19879,7 @@ async function run6(argv, io) {
|
|
|
19879
19879
|
|
|
19880
19880
|
// src/query/index.ts
|
|
19881
19881
|
import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
|
|
19882
|
-
import { resolve as
|
|
19882
|
+
import { resolve as resolve11 } from "node:path";
|
|
19883
19883
|
|
|
19884
19884
|
// src/query/args.ts
|
|
19885
19885
|
var MODE_FLAGS = {
|
|
@@ -20461,7 +20461,7 @@ async function run7(argv, io) {
|
|
|
20461
20461
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
20462
20462
|
try {
|
|
20463
20463
|
const options = parseQueryArgs(argv, process.cwd());
|
|
20464
|
-
root =
|
|
20464
|
+
root = resolve11(options.root);
|
|
20465
20465
|
if (!(existsSync12(root) && statSync10(root).isDirectory())) {
|
|
20466
20466
|
throw new Error("the root <root> is not an existing directory");
|
|
20467
20467
|
}
|
|
@@ -20497,7 +20497,7 @@ async function run7(argv, io) {
|
|
|
20497
20497
|
|
|
20498
20498
|
// src/ste/index.ts
|
|
20499
20499
|
import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
|
|
20500
|
-
import { basename as
|
|
20500
|
+
import { basename as basename11, join as join30, resolve as resolve13 } from "node:path";
|
|
20501
20501
|
|
|
20502
20502
|
// src/ste/markdown.ts
|
|
20503
20503
|
function stripNoise(text) {
|
|
@@ -20552,7 +20552,12 @@ function wordCount(sentence) {
|
|
|
20552
20552
|
const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
|
|
20553
20553
|
return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
|
|
20554
20554
|
}
|
|
20555
|
-
|
|
20555
|
+
var LIST_JOINS = new Set(["and", "or"]);
|
|
20556
|
+
function isIdentifierList(sentence) {
|
|
20557
|
+
const s = sentence.replace(CODE_SPAN, " ").replace(LINK, "$1").replace(LIST_MARKER, "");
|
|
20558
|
+
return pySplit(s).map((w) => Array.from(w).filter(isAlnum).join("").toLowerCase()).filter((w) => w !== "").every((w) => LIST_JOINS.has(w));
|
|
20559
|
+
}
|
|
20560
|
+
function join29(parts2, start2) {
|
|
20556
20561
|
const chunks = [];
|
|
20557
20562
|
const lineMap = [];
|
|
20558
20563
|
parts2.forEach(([n, body2], i2) => {
|
|
@@ -20582,7 +20587,7 @@ function paragraphs(text) {
|
|
|
20582
20587
|
for (const [n, line] of stripNoise(text)) {
|
|
20583
20588
|
const startsItem = LIST_ITEM.test(line);
|
|
20584
20589
|
if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
|
|
20585
|
-
blocks.push(
|
|
20590
|
+
blocks.push(join29(para, paraStart));
|
|
20586
20591
|
para = [];
|
|
20587
20592
|
}
|
|
20588
20593
|
if (para.length === 0)
|
|
@@ -20591,42 +20596,112 @@ function paragraphs(text) {
|
|
|
20591
20596
|
prevLine = n;
|
|
20592
20597
|
}
|
|
20593
20598
|
if (para.length > 0)
|
|
20594
|
-
blocks.push(
|
|
20599
|
+
blocks.push(join29(para, paraStart));
|
|
20595
20600
|
return blocks;
|
|
20596
20601
|
}
|
|
20597
|
-
function
|
|
20598
|
-
const
|
|
20602
|
+
function checkMarkdownFindings(name2, text) {
|
|
20603
|
+
const found = [];
|
|
20599
20604
|
const blocks = paragraphs(text);
|
|
20600
|
-
for (const [start2, block] of blocks) {
|
|
20605
|
+
for (const [start2, block, lineMap] of blocks) {
|
|
20601
20606
|
const procedural = PROCEDURAL_STARTS.some((p) => pyLstrip(block).startsWith(p));
|
|
20602
20607
|
const limit = procedural ? MAX_WORDS_PROCEDURAL : MAX_WORDS_DESCRIPTIVE;
|
|
20608
|
+
const last = lineMap.length > 0 ? lineMap[lineMap.length - 1] : start2;
|
|
20603
20609
|
for (const sentence of sentences(block)) {
|
|
20604
20610
|
const n = wordCount(sentence);
|
|
20605
|
-
if (n > limit) {
|
|
20611
|
+
if (n > limit && !isIdentifierList(sentence)) {
|
|
20606
20612
|
const tier = procedural ? "procedural" : "descriptive";
|
|
20607
|
-
|
|
20613
|
+
found.push({
|
|
20614
|
+
text: `${name2}:~${start2}: ${n} words (>${limit} ${tier}): ${headCodePoints(pyStrip(sentence), 90)}`,
|
|
20615
|
+
first: start2,
|
|
20616
|
+
last
|
|
20617
|
+
});
|
|
20608
20618
|
}
|
|
20609
20619
|
}
|
|
20610
20620
|
}
|
|
20611
20621
|
for (const [, block, lineMap] of blocks) {
|
|
20612
20622
|
const bare = proseOnly(block);
|
|
20623
|
+
const at = (index, rest) => {
|
|
20624
|
+
const line = lineAt2(index, lineMap);
|
|
20625
|
+
return { text: `${name2}:${line}: ${rest}`, first: line, last: line };
|
|
20626
|
+
};
|
|
20613
20627
|
for (const [phrase, better, re] of WORDY_RULES) {
|
|
20614
20628
|
for (const m of bare.matchAll(re)) {
|
|
20615
|
-
|
|
20629
|
+
found.push(at(m.index, `wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`));
|
|
20616
20630
|
}
|
|
20617
20631
|
}
|
|
20618
20632
|
for (const m of bare.matchAll(AMBIGUOUS2)) {
|
|
20619
|
-
|
|
20633
|
+
found.push(at(m.index, `ambiguous reference: ${pyRepr(pyStrip(m[0]))}`));
|
|
20620
20634
|
}
|
|
20621
20635
|
for (const m of bare.matchAll(PASSIVE2)) {
|
|
20622
|
-
|
|
20636
|
+
found.push(at(m.index, `passive voice: ${pyRepr(pyStrip(m[0]))}`));
|
|
20623
20637
|
}
|
|
20624
20638
|
}
|
|
20625
|
-
return
|
|
20639
|
+
return found;
|
|
20640
|
+
}
|
|
20641
|
+
|
|
20642
|
+
// src/ste/since.ts
|
|
20643
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
20644
|
+
import { basename as basename10, dirname as dirname8, resolve as resolve12 } from "node:path";
|
|
20645
|
+
var GIT_TIMEOUT_MS = 60000;
|
|
20646
|
+
|
|
20647
|
+
class SinceError extends Error {
|
|
20648
|
+
}
|
|
20649
|
+
function git(dir, args2) {
|
|
20650
|
+
return spawnSync3("git", ["-C", dir, ...args2], {
|
|
20651
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20652
|
+
timeout: GIT_TIMEOUT_MS,
|
|
20653
|
+
maxBuffer: 1 << 28
|
|
20654
|
+
});
|
|
20655
|
+
}
|
|
20656
|
+
var checked = new Set;
|
|
20657
|
+
function checkRevision(dir, rev) {
|
|
20658
|
+
const key = `${dir}\x00${rev}`;
|
|
20659
|
+
if (checked.has(key))
|
|
20660
|
+
return;
|
|
20661
|
+
const tree = git(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
20662
|
+
if (tree.error)
|
|
20663
|
+
throw new SinceError(`--since needs git, and git cannot run: ${tree.error.message}`);
|
|
20664
|
+
if (tree.status !== 0)
|
|
20665
|
+
throw new SinceError(`--since needs a git work tree: ${dir}`);
|
|
20666
|
+
const commit = git(dir, ["rev-parse", "--verify", "--quiet", `${rev}^{commit}`]);
|
|
20667
|
+
if (commit.status !== 0)
|
|
20668
|
+
throw new SinceError(`cannot resolve revision '${rev}' in ${dir}`);
|
|
20669
|
+
checked.add(key);
|
|
20670
|
+
}
|
|
20671
|
+
var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gmu;
|
|
20672
|
+
function addedLinesSince(file, rev) {
|
|
20673
|
+
const full = resolve12(file);
|
|
20674
|
+
const dir = dirname8(full);
|
|
20675
|
+
const name2 = basename10(full);
|
|
20676
|
+
checkRevision(dir, rev);
|
|
20677
|
+
if (git(dir, ["cat-file", "-e", `${rev}:./${name2}`]).status !== 0)
|
|
20678
|
+
return "all";
|
|
20679
|
+
const diff = git(dir, ["diff", "-U0", "--no-color", "--no-ext-diff", rev, "--", name2]);
|
|
20680
|
+
if (diff.status !== 0) {
|
|
20681
|
+
throw new SinceError(`git diff failed for ${name2}: ${diff.stderr.toString().trim()}`);
|
|
20682
|
+
}
|
|
20683
|
+
const ranges = [];
|
|
20684
|
+
const cuts = [];
|
|
20685
|
+
for (const m of diff.stdout.toString().matchAll(HUNK)) {
|
|
20686
|
+
const first = Number(m[1]);
|
|
20687
|
+
const count = m[2] === undefined ? 1 : Number(m[2]);
|
|
20688
|
+
if (count > 0)
|
|
20689
|
+
ranges.push([first, first + count - 1]);
|
|
20690
|
+
else
|
|
20691
|
+
cuts.push(first);
|
|
20692
|
+
}
|
|
20693
|
+
return { ranges, cuts };
|
|
20694
|
+
}
|
|
20695
|
+
function touchesAdded(added, first, last) {
|
|
20696
|
+
if (added === "all")
|
|
20697
|
+
return true;
|
|
20698
|
+
if (added.ranges.some(([from, to]) => from <= last && first <= to))
|
|
20699
|
+
return true;
|
|
20700
|
+
return added.cuts.some((line) => first <= line && line + 1 <= last);
|
|
20626
20701
|
}
|
|
20627
20702
|
|
|
20628
20703
|
// src/ste/index.ts
|
|
20629
|
-
var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...]
|
|
20704
|
+
var STE_HELP = `Usage: repo-tools ste <file-or-directory> [...] [--require=<path>] [--since=<rev>]
|
|
20630
20705
|
repo-tools ste --prose <file>
|
|
20631
20706
|
|
|
20632
20707
|
Check prose against the mechanically decidable part of ASD-STE100 Simplified
|
|
@@ -20636,11 +20711,25 @@ cannot judge the approved dictionary.
|
|
|
20636
20711
|
|
|
20637
20712
|
Markdown mode (the default) checks each Markdown file, or each .md file below a
|
|
20638
20713
|
directory, in code-unit order of the path. A descriptive sentence may have 25
|
|
20639
|
-
words; a procedural one (a numbered step or a check box) may have 20.
|
|
20640
|
-
|
|
20641
|
-
|
|
20714
|
+
words; a procedural one (a numbered step or a check box) may have 20. A list of
|
|
20715
|
+
identifiers with no prose word is not a sentence, so the word limit does not
|
|
20716
|
+
apply to it. Code fences, tables, headings, quotes, comments and frontmatter are
|
|
20717
|
+
not checked. Inline code and identifiers are not checked by the wordy, ambiguous
|
|
20718
|
+
and passive rules, and each one counts as a word. A file whose first five lines
|
|
20719
|
+
hold
|
|
20642
20720
|
"<!-- ste:historical-record -->" is skipped, and the skip is counted.
|
|
20643
20721
|
|
|
20722
|
+
--require=<path> The file must exist, and the run must examine it. Repeat the
|
|
20723
|
+
option for more files. A document that someone deletes inside
|
|
20724
|
+
a folder gives no finding, so this option makes the deletion
|
|
20725
|
+
an error. A file that the run skips as a historical record
|
|
20726
|
+
counts as examined.
|
|
20727
|
+
--since=<rev> Report only the findings on the lines that the working tree
|
|
20728
|
+
adds or changes since the git revision <rev> (committed,
|
|
20729
|
+
staged and unstaged edits). A sentence-length finding counts
|
|
20730
|
+
when any line of its paragraph changed. A file that is not in
|
|
20731
|
+
<rev> counts in full. The totals line gives the number of
|
|
20732
|
+
older findings that the option hides.
|
|
20644
20733
|
--prose <file> Check the file as one docstring. Tag lines (@param), section
|
|
20645
20734
|
headers, doctest lines and code fences are removed first. Each
|
|
20646
20735
|
finding prints as "RULE<TAB>detail". The findings are advice:
|
|
@@ -20649,7 +20738,9 @@ identifiers are not checked. A file whose first five lines hold
|
|
|
20649
20738
|
|
|
20650
20739
|
Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
|
|
20651
20740
|
Markdown finding exists. 2 on a usage error, when a path does not exist, when no
|
|
20652
|
-
Markdown file was examined,
|
|
20741
|
+
Markdown file was examined, when a required file does not exist or was not
|
|
20742
|
+
examined, when --since cannot use the revision or the work tree, or when a file
|
|
20743
|
+
is not UTF-8.
|
|
20653
20744
|
`;
|
|
20654
20745
|
var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
|
|
20655
20746
|
function readText(path) {
|
|
@@ -20678,7 +20769,7 @@ function targetsFor(path) {
|
|
|
20678
20769
|
return;
|
|
20679
20770
|
}
|
|
20680
20771
|
for (const entry of entries) {
|
|
20681
|
-
const full =
|
|
20772
|
+
const full = join30(dir, entry.name);
|
|
20682
20773
|
if (entry.isDirectory()) {
|
|
20683
20774
|
if (entry.name !== ".pytest_cache")
|
|
20684
20775
|
walk(full, [...parts2, entry.name]);
|
|
@@ -20691,7 +20782,7 @@ function targetsFor(path) {
|
|
|
20691
20782
|
};
|
|
20692
20783
|
if (existsSync13(path) && lstatSync6(path).isDirectory())
|
|
20693
20784
|
walk(path, []);
|
|
20694
|
-
return found.sort(comparePaths).map((parts2) =>
|
|
20785
|
+
return found.sort(comparePaths).map((parts2) => join30(path, ...parts2));
|
|
20695
20786
|
}
|
|
20696
20787
|
function isHistoricalRecord(path) {
|
|
20697
20788
|
let text;
|
|
@@ -20713,7 +20804,7 @@ function runProse(args2, io) {
|
|
|
20713
20804
|
try {
|
|
20714
20805
|
text = readText(file);
|
|
20715
20806
|
} catch {
|
|
20716
|
-
io.stderr(`repo-tools ste: cannot read ${
|
|
20807
|
+
io.stderr(`repo-tools ste: cannot read ${basename11(file)} as UTF-8 text
|
|
20717
20808
|
`);
|
|
20718
20809
|
return 2;
|
|
20719
20810
|
}
|
|
@@ -20722,10 +20813,41 @@ function runProse(args2, io) {
|
|
|
20722
20813
|
`);
|
|
20723
20814
|
return 0;
|
|
20724
20815
|
}
|
|
20816
|
+
function parseMarkdownArgs(argv) {
|
|
20817
|
+
const args2 = { paths: [], required: [], since: null };
|
|
20818
|
+
for (const arg of argv) {
|
|
20819
|
+
if (arg === "--require" || arg.startsWith("--require=")) {
|
|
20820
|
+
const value = arg.slice("--require=".length);
|
|
20821
|
+
if (arg === "--require" || value === "")
|
|
20822
|
+
return "--require needs a path (--require=<path>)";
|
|
20823
|
+
args2.required.push(value);
|
|
20824
|
+
} else if (arg === "--since" || arg.startsWith("--since=")) {
|
|
20825
|
+
const value = arg.slice("--since=".length);
|
|
20826
|
+
if (arg === "--since" || value === "" || value.startsWith("-")) {
|
|
20827
|
+
return "--since needs a revision (--since=<rev>)";
|
|
20828
|
+
}
|
|
20829
|
+
args2.since = value;
|
|
20830
|
+
} else {
|
|
20831
|
+
args2.paths.push(arg);
|
|
20832
|
+
}
|
|
20833
|
+
}
|
|
20834
|
+
return args2;
|
|
20835
|
+
}
|
|
20836
|
+
function pathKey(path) {
|
|
20837
|
+
const full = resolve13(path);
|
|
20838
|
+
return process.platform === "win32" ? full.toLowerCase() : full;
|
|
20839
|
+
}
|
|
20725
20840
|
async function run8(argv, io) {
|
|
20726
20841
|
if (argv[0] === "--prose")
|
|
20727
20842
|
return runProse(argv.slice(1), io);
|
|
20728
|
-
|
|
20843
|
+
const parsed = parseMarkdownArgs(argv);
|
|
20844
|
+
if (typeof parsed === "string") {
|
|
20845
|
+
io.stderr(`repo-tools ste: ${parsed}
|
|
20846
|
+
`);
|
|
20847
|
+
return 2;
|
|
20848
|
+
}
|
|
20849
|
+
const { paths, required, since } = parsed;
|
|
20850
|
+
if (paths.length === 0) {
|
|
20729
20851
|
io.stdout(`usage: repo-tools ste <file-or-directory> [...]
|
|
20730
20852
|
`);
|
|
20731
20853
|
return 2;
|
|
@@ -20733,12 +20855,15 @@ async function run8(argv, io) {
|
|
|
20733
20855
|
let total = 0;
|
|
20734
20856
|
let checked = 0;
|
|
20735
20857
|
let skipped = 0;
|
|
20858
|
+
let hidden = 0;
|
|
20736
20859
|
const missing = [];
|
|
20737
|
-
|
|
20860
|
+
const examined = new Set;
|
|
20861
|
+
for (const arg of paths) {
|
|
20738
20862
|
if (!existsSync13(arg))
|
|
20739
20863
|
missing.push(arg);
|
|
20740
20864
|
for (const target of targetsFor(arg)) {
|
|
20741
|
-
const name2 =
|
|
20865
|
+
const name2 = basename11(target);
|
|
20866
|
+
examined.add(pathKey(target));
|
|
20742
20867
|
if (isHistoricalRecord(target)) {
|
|
20743
20868
|
skipped += 1;
|
|
20744
20869
|
io.stdout(`
|
|
@@ -20754,30 +20879,62 @@ async function run8(argv, io) {
|
|
|
20754
20879
|
`);
|
|
20755
20880
|
return 2;
|
|
20756
20881
|
}
|
|
20757
|
-
|
|
20882
|
+
let findings = checkMarkdownFindings(name2, text);
|
|
20883
|
+
if (since !== null) {
|
|
20884
|
+
try {
|
|
20885
|
+
const added = addedLinesSince(target, since);
|
|
20886
|
+
const shown = findings.filter((f) => touchesAdded(added, f.first, f.last));
|
|
20887
|
+
hidden += findings.length - shown.length;
|
|
20888
|
+
findings = shown;
|
|
20889
|
+
} catch (error) {
|
|
20890
|
+
if (!(error instanceof SinceError))
|
|
20891
|
+
throw error;
|
|
20892
|
+
io.stderr(`repo-tools ste: ${error.message}
|
|
20893
|
+
`);
|
|
20894
|
+
return 2;
|
|
20895
|
+
}
|
|
20896
|
+
}
|
|
20758
20897
|
checked += 1;
|
|
20759
|
-
total +=
|
|
20760
|
-
const status =
|
|
20898
|
+
total += findings.length;
|
|
20899
|
+
const status = findings.length === 0 ? "OK" : `${findings.length} problem(s)`;
|
|
20761
20900
|
io.stdout(`
|
|
20762
20901
|
=== ${name2}: ${status} ===
|
|
20763
20902
|
`);
|
|
20764
|
-
for (const
|
|
20765
|
-
io.stdout(` ${
|
|
20903
|
+
for (const f of findings)
|
|
20904
|
+
io.stdout(` ${f.text}
|
|
20766
20905
|
`);
|
|
20767
20906
|
}
|
|
20768
20907
|
}
|
|
20769
|
-
const
|
|
20908
|
+
const notes = [];
|
|
20909
|
+
if (skipped > 0)
|
|
20910
|
+
notes.push(`${skipped} skipped as historical record(s)`);
|
|
20911
|
+
if (since !== null && hidden > 0) {
|
|
20912
|
+
notes.push(`${hidden} older finding(s) hidden by --since=${since}`);
|
|
20913
|
+
}
|
|
20914
|
+
const tail = notes.length > 0 ? `; ${notes.join("; ")}` : "";
|
|
20770
20915
|
io.stdout(`
|
|
20771
20916
|
TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
|
|
20772
20917
|
`);
|
|
20773
20918
|
for (const path of missing)
|
|
20774
20919
|
io.stderr(`ERROR: path does not exist: ${path}
|
|
20775
20920
|
`);
|
|
20921
|
+
let requiredFailed = false;
|
|
20922
|
+
for (const path of required) {
|
|
20923
|
+
if (!existsSync13(path) || !statSync11(path).isFile()) {
|
|
20924
|
+
io.stderr(`ERROR: required file does not exist: ${path}
|
|
20925
|
+
`);
|
|
20926
|
+
requiredFailed = true;
|
|
20927
|
+
} else if (!examined.has(pathKey(path))) {
|
|
20928
|
+
io.stderr(`ERROR: required file was not examined: ${path}
|
|
20929
|
+
`);
|
|
20930
|
+
requiredFailed = true;
|
|
20931
|
+
}
|
|
20932
|
+
}
|
|
20776
20933
|
if (checked === 0) {
|
|
20777
20934
|
io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
|
|
20778
20935
|
`);
|
|
20779
20936
|
}
|
|
20780
|
-
if (missing.length > 0 || checked === 0)
|
|
20937
|
+
if (missing.length > 0 || checked === 0 || requiredFailed)
|
|
20781
20938
|
return 2;
|
|
20782
20939
|
return total > 0 ? 1 : 0;
|
|
20783
20940
|
}
|