@danielsimonjr/repo-tools 2.2.7 → 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 +97 -0
- package/README.md +60 -2
- package/dist/cli.js +1981 -1143
- 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,864 +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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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");
|
|
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);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (def)
|
|
1001
|
+
names.add(def);
|
|
1002
|
+
if (ns)
|
|
1003
|
+
names.add(ns);
|
|
854
1004
|
}
|
|
1005
|
+
return names;
|
|
855
1006
|
}
|
|
856
|
-
function
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (kind === undefined)
|
|
861
|
-
throw fail(`unknown key '${prefix}${name2}'`);
|
|
862
|
-
out2[name2] = checkValue(`${prefix}${name2}`, kind, value, fail);
|
|
863
|
-
}
|
|
864
|
-
return out2;
|
|
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] ?? "");
|
|
865
1011
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
}
|
|
879
1025
|
}
|
|
880
1026
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
+
}
|
|
896
1043
|
}
|
|
897
1044
|
}
|
|
898
|
-
|
|
899
|
-
if (query !== undefined) {
|
|
900
|
-
if (!isObject(query))
|
|
901
|
-
throw fail("'query' must be an object");
|
|
902
|
-
sections.query = checkSection("query.", query, QUERY_KEYS, fail);
|
|
903
|
-
}
|
|
904
|
-
return sections;
|
|
1045
|
+
return byName;
|
|
905
1046
|
}
|
|
906
|
-
function
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
const rel = configPath ?? CONFIG_FILE;
|
|
911
|
-
const label = `<root>/${rel.replace(/\\/g, "/")}`;
|
|
912
|
-
const path = resolveUnderRoot(root, rel);
|
|
913
|
-
if (configPath === undefined && !existsSync2(path))
|
|
914
|
-
return { depgraph: {}, query: {} };
|
|
915
|
-
let text;
|
|
916
|
-
try {
|
|
917
|
-
text = readFileSync(path, "utf8");
|
|
918
|
-
} catch {
|
|
919
|
-
throw new Error(`config ${label}: the file cannot be read`);
|
|
920
|
-
}
|
|
921
|
-
let parsed;
|
|
922
|
-
try {
|
|
923
|
-
parsed = JSON.parse(text);
|
|
924
|
-
} catch {
|
|
925
|
-
throw new Error(`config ${label}: the file is not valid JSON`);
|
|
926
|
-
}
|
|
927
|
-
return parseConfigSections(parsed, label);
|
|
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 };
|
|
928
1051
|
}
|
|
929
|
-
function
|
|
930
|
-
|
|
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));
|
|
1090
|
+
}
|
|
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;
|
|
931
1093
|
}
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1094
|
+
function tallyByTag(entries) {
|
|
1095
|
+
const tally = {
|
|
1096
|
+
TRUE_DUPLICATE: 0,
|
|
1097
|
+
DISPATCH_VARIANT: 0,
|
|
1098
|
+
ALIAS_DELEGATION: 0,
|
|
1099
|
+
ALLOWLISTED: 0
|
|
936
1100
|
};
|
|
1101
|
+
for (const e of entries)
|
|
1102
|
+
tally[e.tag]++;
|
|
1103
|
+
return tally;
|
|
937
1104
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
const
|
|
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
|
+
};
|
|
942
1120
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inDocs("duplicate-baseline.json"),
|
|
946
|
-
coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inDocs("coverage-policy.json"),
|
|
947
|
-
regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? MAP_REGENERATE_COMMAND
|
|
1121
|
+
runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls),
|
|
1122
|
+
types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw, dispatchCalls)
|
|
948
1123
|
};
|
|
949
1124
|
}
|
|
950
|
-
function
|
|
951
|
-
const
|
|
952
|
-
const
|
|
1125
|
+
function buildDuplicateReport(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
1126
|
+
const runtimeByTag = tallyByTag(dup.runtime);
|
|
1127
|
+
const typeByTag = tallyByTag(dup.types);
|
|
953
1128
|
return {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
duplicateAllowlist: cli.duplicateAllowlist ?? file.duplicateAllowlist ?? inOut("duplicate-allowlist.json"),
|
|
961
|
-
duplicateBaseline: cli.duplicateBaseline ?? file.duplicateBaseline ?? inOut("duplicate-baseline.json"),
|
|
962
|
-
coveragePolicy: cli.coveragePolicy ?? file.coveragePolicy ?? inOut("coverage-policy.json"),
|
|
963
|
-
regenerateCommand: cli.regenerateCommand ?? file.regenerateCommand ?? DEFAULT_REGENERATE_COMMAND,
|
|
964
|
-
verificationMarker: cli.verificationMarker !== undefined ? cli.verificationMarker : file.verificationMarker !== undefined ? file.verificationMarker : VERIFICATION_MARKER,
|
|
965
|
-
apiSurface: {
|
|
966
|
-
out: cli.apiSurface?.out !== undefined ? cli.apiSurface.out : file.apiSurface?.out !== undefined ? file.apiSurface.out : null,
|
|
967
|
-
entry: cli.apiSurface?.entry ?? file.apiSurface?.entry ?? DEFAULT_API_ENTRY,
|
|
968
|
-
stabilityTags: cli.apiSurface?.stabilityTags ?? file.apiSurface?.stabilityTags ?? [...DEFAULT_STABILITY_TAGS]
|
|
1129
|
+
note: duplicateSymbolsNote(dispatchCalls),
|
|
1130
|
+
summary: {
|
|
1131
|
+
runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
|
|
1132
|
+
typeDuplicates: typeByTag.TRUE_DUPLICATE,
|
|
1133
|
+
runtimeByTag,
|
|
1134
|
+
typeByTag
|
|
969
1135
|
},
|
|
970
|
-
|
|
1136
|
+
runtime: dup.runtime,
|
|
1137
|
+
types: dup.types
|
|
971
1138
|
};
|
|
972
1139
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
"--all": "the graph always holds every file; the disposition of each file shows reachability",
|
|
981
|
-
"-a": "the graph always holds every file; the disposition of each file shows reachability",
|
|
982
|
-
"--reachable-only": "the graph always holds every file; the disposition shows reachability",
|
|
983
|
-
"--include-tests": "the test coverage reports are always written",
|
|
984
|
-
"-t": "the test coverage reports are always written"
|
|
985
|
-
};
|
|
986
|
-
function scanScopeText(flag) {
|
|
987
|
-
return `flag ${flag} has no effect in 2.0.0: ${SCAN_SCOPE_FLAGS[flag]}`;
|
|
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;
|
|
988
1147
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
"
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
};
|
|
1005
|
-
var VALUE_FLAGS = {
|
|
1006
|
-
"--root": (_o, value, setRoot) => setRoot(value),
|
|
1007
|
-
"--config": (o, value) => {
|
|
1008
|
-
o.config = relativePath("--config", value);
|
|
1009
|
-
},
|
|
1010
|
-
"--src": (o, value) => {
|
|
1011
|
-
o.settings.src = value === "auto" ? "auto" : pathList("--src", value);
|
|
1012
|
-
},
|
|
1013
|
-
"--tests": (o, value) => {
|
|
1014
|
-
o.settings.tests = pathList("--tests", value);
|
|
1015
|
-
},
|
|
1016
|
-
"--out": (o, value) => {
|
|
1017
|
-
o.settings.out = relativePath("--out", value);
|
|
1018
|
-
},
|
|
1019
|
-
"--exclude": (o, value) => {
|
|
1020
|
-
o.settings.exclude = list("--exclude", value);
|
|
1021
|
-
},
|
|
1022
|
-
"--also-exclude": (o, value) => {
|
|
1023
|
-
o.settings.alsoExclude = list("--also-exclude", value);
|
|
1024
|
-
},
|
|
1025
|
-
"--api-surface": (o, value) => {
|
|
1026
|
-
o.settings.apiSurface = { ...o.settings.apiSurface, out: relativePath("--api-surface", value) };
|
|
1027
|
-
},
|
|
1028
|
-
"--api-entry": (o, value) => {
|
|
1029
|
-
o.settings.apiSurface = { ...o.settings.apiSurface, entry: relativePath("--api-entry", value) };
|
|
1030
|
-
},
|
|
1031
|
-
"--stability-tags": (o, value) => {
|
|
1032
|
-
o.settings.apiSurface = {
|
|
1033
|
-
...o.settings.apiSurface,
|
|
1034
|
-
stabilityTags: list("--stability-tags", value)
|
|
1035
|
-
};
|
|
1148
|
+
function buildDuplicateBaseline(report) {
|
|
1149
|
+
return {
|
|
1150
|
+
note: DUPLICATE_BASELINE_NOTE,
|
|
1151
|
+
runtime: trueDuplicateNames(report.runtime),
|
|
1152
|
+
types: trueDuplicateNames(report.types)
|
|
1153
|
+
};
|
|
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
|
+
}
|
|
1036
1163
|
}
|
|
1037
|
-
|
|
1038
|
-
function list(flag, value) {
|
|
1039
|
-
const items = value.split(",");
|
|
1040
|
-
if (items.some((item) => item === ""))
|
|
1041
|
-
throw new Error(`flag ${flag} holds an empty item`);
|
|
1042
|
-
return items;
|
|
1164
|
+
return found;
|
|
1043
1165
|
}
|
|
1044
|
-
function
|
|
1045
|
-
return
|
|
1166
|
+
function isRecord(value) {
|
|
1167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1046
1168
|
}
|
|
1047
|
-
function
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1169
|
+
function asDuplicateEntries(parsed) {
|
|
1170
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.runtime) || !Array.isArray(parsed.types)) {
|
|
1171
|
+
return null;
|
|
1050
1172
|
}
|
|
1051
|
-
|
|
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;
|
|
1052
1177
|
}
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
strictOrphans: false,
|
|
1061
|
-
strictCensus: false,
|
|
1062
|
-
checkCensus: false,
|
|
1063
|
-
checkDuplicates: false,
|
|
1064
|
-
noRegen: false,
|
|
1065
|
-
writeDuplicateBaseline: false,
|
|
1066
|
-
noExtensions: false,
|
|
1067
|
-
help: false,
|
|
1068
|
-
ignoredFlags: []
|
|
1069
|
-
};
|
|
1070
|
-
let rootSet = false;
|
|
1071
|
-
const setRoot = (value) => {
|
|
1072
|
-
if (rootSet)
|
|
1073
|
-
throw new Error("the root is set twice (use --root=<path> or one positional path)");
|
|
1074
|
-
rootSet = true;
|
|
1075
|
-
options.root = value;
|
|
1076
|
-
};
|
|
1077
|
-
for (const arg of argv) {
|
|
1078
|
-
if (!arg.startsWith("-")) {
|
|
1079
|
-
setRoot(arg);
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
const eq = arg.indexOf("=");
|
|
1083
|
-
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
1084
|
-
const value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
1085
|
-
if (command !== undefined && name2 in SCAN_SCOPE_FLAGS) {
|
|
1086
|
-
if (command === "map")
|
|
1087
|
-
throw new Error(scanScopeText(name2));
|
|
1088
|
-
const takesValue = name2 in VALUE_FLAGS;
|
|
1089
|
-
if (!takesValue && value !== undefined)
|
|
1090
|
-
throw new Error(`flag ${name2} takes no value`);
|
|
1091
|
-
if (takesValue && (value === undefined || value === "")) {
|
|
1092
|
-
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1093
|
-
}
|
|
1094
|
-
options.ignoredFlags.push(name2);
|
|
1095
|
-
continue;
|
|
1096
|
-
}
|
|
1097
|
-
const booleanKey = BOOLEAN_FLAGS[name2];
|
|
1098
|
-
if (booleanKey !== undefined) {
|
|
1099
|
-
if (value !== undefined)
|
|
1100
|
-
throw new Error(`flag ${name2} takes no value`);
|
|
1101
|
-
options[booleanKey] = true;
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
const apply = VALUE_FLAGS[name2];
|
|
1105
|
-
if (apply !== undefined) {
|
|
1106
|
-
if (value === undefined || value === "") {
|
|
1107
|
-
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
1108
|
-
}
|
|
1109
|
-
apply(options, value, setRoot);
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
|
|
1113
|
-
}
|
|
1114
|
-
checkModes(options);
|
|
1115
|
-
return options;
|
|
1116
|
-
}
|
|
1117
|
-
function checkModes(options) {
|
|
1118
|
-
if (options.noRegen && !options.checkDuplicates) {
|
|
1119
|
-
throw new Error("flag --no-regen applies with --check-duplicates only");
|
|
1120
|
-
}
|
|
1121
|
-
const modes = [
|
|
1122
|
-
options.checkCensus && "--check-census",
|
|
1123
|
-
options.checkDuplicates && "--check-duplicates",
|
|
1124
|
-
options.writeDuplicateBaseline && "--write-duplicate-baseline"
|
|
1125
|
-
].filter((mode) => mode !== false);
|
|
1126
|
-
if (modes.length > 1) {
|
|
1127
|
-
throw new Error(`flags ${modes.join(" and ")} are two modes; use one of them in a run`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
function wantsHelp(argv) {
|
|
1131
|
-
return argv.includes("--help") || argv.includes("-h");
|
|
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;
|
|
1132
1185
|
}
|
|
1133
1186
|
|
|
1134
|
-
// src/
|
|
1135
|
-
import {
|
|
1136
|
-
import { resolve as resolve4 } from "node:path";
|
|
1137
|
-
|
|
1138
|
-
// src/depgraph/duplicates.ts
|
|
1139
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1187
|
+
// src/depgraph/exclude.ts
|
|
1188
|
+
import { statSync } from "node:fs";
|
|
1140
1189
|
import { join as join4 } from "node:path";
|
|
1141
1190
|
|
|
1142
|
-
// src/
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
return true;
|
|
1162
|
-
const c = src[prev] ?? "";
|
|
1163
|
-
if (c === ")")
|
|
1164
|
-
return afterStmtParen;
|
|
1165
|
-
if (c === ">")
|
|
1166
|
-
return src[prev - 1] === "=";
|
|
1167
|
-
if ((c === "+" || c === "-") && src[prev - 1] === c)
|
|
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 })
|
|
1197
|
+
};
|
|
1198
|
+
function listNames(dir) {
|
|
1199
|
+
return sortCodeUnits(readdirHook.names(dir));
|
|
1200
|
+
}
|
|
1201
|
+
function listEntries(dir) {
|
|
1202
|
+
return [...readdirHook.entries(dir)].sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
1203
|
+
}
|
|
1204
|
+
var linkLog = { skipped: new Set };
|
|
1205
|
+
function isLink(path) {
|
|
1206
|
+
let link;
|
|
1207
|
+
try {
|
|
1208
|
+
link = lstatSync(path).isSymbolicLink();
|
|
1209
|
+
} catch {
|
|
1168
1210
|
return false;
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1211
|
+
}
|
|
1212
|
+
if (link)
|
|
1213
|
+
linkLog.skipped.add(path);
|
|
1214
|
+
return link;
|
|
1215
|
+
}
|
|
1216
|
+
function isLinkEntry(dir, entry) {
|
|
1217
|
+
if (!entry.isSymbolicLink())
|
|
1172
1218
|
return false;
|
|
1173
|
-
|
|
1174
|
-
return
|
|
1219
|
+
linkLog.skipped.add(join3(dir, entry.name));
|
|
1220
|
+
return true;
|
|
1175
1221
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1222
|
+
function isWalkable(dir) {
|
|
1223
|
+
return existsSync(dir) && !isLink(dir);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/depgraph/exclude.ts
|
|
1227
|
+
var DEFAULT_EXCLUDE = [
|
|
1228
|
+
"node_modules",
|
|
1229
|
+
"dist",
|
|
1230
|
+
"build",
|
|
1231
|
+
"coverage",
|
|
1232
|
+
".git"
|
|
1233
|
+
];
|
|
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);
|
|
1237
|
+
}
|
|
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);
|
|
1191
1251
|
}
|
|
1192
|
-
return
|
|
1252
|
+
return files;
|
|
1193
1253
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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}`;
|
|
1267
|
+
}
|
|
1268
|
+
function withBanner(body2, options = {}) {
|
|
1269
|
+
return bannerFor(options) + body2;
|
|
1270
|
+
}
|
|
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);
|
|
1278
|
+
}
|
|
1279
|
+
function resolveUnderRoot(root, path) {
|
|
1280
|
+
return resolve(root, path);
|
|
1281
|
+
}
|
|
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);
|
|
1305
|
+
}
|
|
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;
|
|
1204
1311
|
};
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
while (i2 < n) {
|
|
1211
|
-
const c = src[i2];
|
|
1212
|
-
if (c === "\\") {
|
|
1213
|
-
i2 += 2;
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
if (c === "`") {
|
|
1217
|
-
push("string", start2, i2);
|
|
1218
|
-
push("code", i2, i2 + 1);
|
|
1219
|
-
i2 += 1;
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
if (c === "$" && src[i2 + 1] === "{") {
|
|
1223
|
-
push("string", start2, i2);
|
|
1224
|
-
push("code", i2, i2 + 2);
|
|
1225
|
-
i2 += 2;
|
|
1226
|
-
templates.push(0);
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
i2 += 1;
|
|
1230
|
-
}
|
|
1231
|
-
push("string", start2, n);
|
|
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;
|
|
1232
1317
|
};
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const wordEndingAt = (index) => {
|
|
1238
|
-
let end = index;
|
|
1239
|
-
while (end > 0) {
|
|
1240
|
-
const prev = src[end - 1] ?? "";
|
|
1241
|
-
if (/\s/.test(prev)) {
|
|
1242
|
-
end -= 1;
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
|
-
if (prev === "/" && src[end - 2] === "*") {
|
|
1246
|
-
const open = src.lastIndexOf("/*", end - 2);
|
|
1247
|
-
if (open === -1)
|
|
1248
|
-
break;
|
|
1249
|
-
end = open;
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
break;
|
|
1253
|
-
}
|
|
1254
|
-
return /[\w$]+$/.exec(src.slice(Math.max(0, end - 16), end))?.[0] ?? "";
|
|
1318
|
+
const list = (v, item, what) => {
|
|
1319
|
+
if (!Array.isArray(v))
|
|
1320
|
+
throw fail(`'${key}' must be ${what}`);
|
|
1321
|
+
return v.map(item);
|
|
1255
1322
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
if (c === "/" && next === "/") {
|
|
1275
|
-
push("code", codeStart, i2);
|
|
1276
|
-
const eol = src.indexOf(`
|
|
1277
|
-
`, i2);
|
|
1278
|
-
const end = eol === -1 ? n : eol;
|
|
1279
|
-
push("comment", i2, end);
|
|
1280
|
-
i2 = end;
|
|
1281
|
-
codeStart = i2;
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
if (c === "/" && next === "*") {
|
|
1285
|
-
push("code", codeStart, i2);
|
|
1286
|
-
const close = src.indexOf("*/", i2 + 2);
|
|
1287
|
-
const end = close === -1 ? n : close + 2;
|
|
1288
|
-
push("comment", i2, end);
|
|
1289
|
-
i2 = end;
|
|
1290
|
-
codeStart = i2;
|
|
1291
|
-
continue;
|
|
1292
|
-
}
|
|
1293
|
-
if (c === "'" || c === '"') {
|
|
1294
|
-
push("code", codeStart, i2 + 1);
|
|
1295
|
-
let j = i2 + 1;
|
|
1296
|
-
while (j < n && src[j] !== c && src[j] !== `
|
|
1297
|
-
`)
|
|
1298
|
-
j += src[j] === "\\" ? 2 : 1;
|
|
1299
|
-
const end = Math.min(j, n);
|
|
1300
|
-
push("string", i2 + 1, end);
|
|
1301
|
-
i2 = end < n && src[end] === c ? end + 1 : end;
|
|
1302
|
-
push("code", end, i2);
|
|
1303
|
-
prev = i2 - 1;
|
|
1304
|
-
closedStmt = false;
|
|
1305
|
-
codeStart = i2;
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
if (c === "`") {
|
|
1309
|
-
push("code", codeStart, i2 + 1);
|
|
1310
|
-
i2 += 1;
|
|
1311
|
-
readTemplateBody();
|
|
1312
|
-
prev = i2 - 1;
|
|
1313
|
-
closedStmt = false;
|
|
1314
|
-
codeStart = i2;
|
|
1315
|
-
continue;
|
|
1316
|
-
}
|
|
1317
|
-
if (!/\s/.test(c ?? "")) {
|
|
1318
|
-
if (c === "(") {
|
|
1319
|
-
const word = wordEndingAt(i2);
|
|
1320
|
-
parenStmt.push(word === "if" || word === "while" || word === "for" || word === "with");
|
|
1321
|
-
closedStmt = false;
|
|
1322
|
-
} else if (c === ")")
|
|
1323
|
-
closedStmt = parenStmt.pop() === true;
|
|
1324
|
-
else
|
|
1325
|
-
closedStmt = false;
|
|
1326
|
-
prev = i2;
|
|
1327
|
-
}
|
|
1328
|
-
if (templates.length > 0) {
|
|
1329
|
-
const depth = templates.length - 1;
|
|
1330
|
-
if (c === "{")
|
|
1331
|
-
templates[depth] = (templates[depth] ?? 0) + 1;
|
|
1332
|
-
else if (c === "}") {
|
|
1333
|
-
if (templates[depth] === 0) {
|
|
1334
|
-
templates.pop();
|
|
1335
|
-
push("code", codeStart, i2 + 1);
|
|
1336
|
-
i2 += 1;
|
|
1337
|
-
readTemplateBody();
|
|
1338
|
-
prev = i2 - 1;
|
|
1339
|
-
closedStmt = false;
|
|
1340
|
-
codeStart = i2;
|
|
1341
|
-
continue;
|
|
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)`);
|
|
1342
1340
|
}
|
|
1343
|
-
|
|
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`);
|
|
1344
1354
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
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;
|
|
1347
1361
|
}
|
|
1348
|
-
push("code", codeStart, n);
|
|
1349
|
-
return out2;
|
|
1350
1362
|
}
|
|
1351
|
-
function
|
|
1352
|
-
|
|
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);
|
|
1370
|
+
}
|
|
1371
|
+
return out2;
|
|
1353
1372
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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));
|
|
1359
1391
|
}
|
|
1360
|
-
function
|
|
1361
|
-
|
|
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")
|
|
1398
|
+
continue;
|
|
1399
|
+
if (key !== "depgraph" && key !== "map" && key !== "query" && key !== "clones") {
|
|
1400
|
+
throw fail(`unknown key '${key}'`);
|
|
1401
|
+
}
|
|
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);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
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;
|
|
1362
1433
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
{
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
ALIAS_DELEGATION: 2,
|
|
1380
|
-
ALLOWLISTED: 3
|
|
1381
|
-
};
|
|
1382
|
-
function loadDuplicateAllowlist(path) {
|
|
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;
|
|
1383
1450
|
try {
|
|
1384
|
-
|
|
1385
|
-
return parsed.entries ?? [];
|
|
1451
|
+
parsed = JSON.parse(text);
|
|
1386
1452
|
} catch {
|
|
1387
|
-
|
|
1453
|
+
throw new Error(`config ${label}: the file is not valid JSON`);
|
|
1388
1454
|
}
|
|
1455
|
+
return parseConfigSections(parsed, label);
|
|
1389
1456
|
}
|
|
1390
|
-
function
|
|
1391
|
-
|
|
1392
|
-
return true;
|
|
1393
|
-
if (pattern.endsWith("/**"))
|
|
1394
|
-
return value.startsWith(pattern.slice(0, -2));
|
|
1395
|
-
if (pattern.endsWith("*"))
|
|
1396
|
-
return value.startsWith(pattern.slice(0, -1));
|
|
1397
|
-
return value === pattern;
|
|
1398
|
-
}
|
|
1399
|
-
function findAllowlistMatch(allowlist, name2, filePath) {
|
|
1400
|
-
return allowlist.find((e) => e.names.some((n) => globMatchSingle(n, name2)) && e.filesGlob.some((f) => globMatchSingle(f, filePath)));
|
|
1457
|
+
function loadConfigFile(root, configPath) {
|
|
1458
|
+
return loadConfigSections(root, configPath).depgraph;
|
|
1401
1459
|
}
|
|
1402
|
-
function
|
|
1403
|
-
return
|
|
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 ?? []
|
|
1464
|
+
};
|
|
1404
1465
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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}`;
|
|
1470
|
+
return {
|
|
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
|
|
1476
|
+
};
|
|
1408
1477
|
}
|
|
1409
|
-
function
|
|
1410
|
-
const
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
return names;
|
|
1478
|
+
function mergeDepgraphConfig(cli, file) {
|
|
1479
|
+
const out2 = cli.out ?? file.out ?? OUTPUT_SUBDIR;
|
|
1480
|
+
const inOut = (name2) => `${out2.replace(/[\\/]+$/, "")}/${name2}`;
|
|
1481
|
+
return {
|
|
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]
|
|
1498
|
+
},
|
|
1499
|
+
extensions: cli.extensions ?? file.extensions ?? []
|
|
1500
|
+
};
|
|
1433
1501
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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]}`;
|
|
1438
1517
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
+
};
|
|
1453
1565
|
}
|
|
1454
|
-
|
|
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;
|
|
1455
1572
|
}
|
|
1456
|
-
function
|
|
1457
|
-
|
|
1458
|
-
for (const file of files) {
|
|
1459
|
-
const reExported = new Set(file.exports.reExported);
|
|
1460
|
-
for (const { cat, key } of categories) {
|
|
1461
|
-
for (const name2 of file.exports[key]) {
|
|
1462
|
-
if (reExported.has(name2))
|
|
1463
|
-
continue;
|
|
1464
|
-
if (key === "types" && file.exports.interfaces.includes(name2))
|
|
1465
|
-
continue;
|
|
1466
|
-
const list = byName.get(name2) ?? [];
|
|
1467
|
-
list.push({ file, category: cat });
|
|
1468
|
-
byName.set(name2, list);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
return byName;
|
|
1573
|
+
function pathList(flag, value) {
|
|
1574
|
+
return list(flag, value).map((item) => relativePath(flag, item));
|
|
1473
1575
|
}
|
|
1474
|
-
function
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1576
|
+
function relativePath(flag, value) {
|
|
1577
|
+
if (isAbsolutePath(value)) {
|
|
1578
|
+
throw new Error(`flag ${flag} ${ABSOLUTE_PATH_TEXT}`);
|
|
1579
|
+
}
|
|
1580
|
+
return value;
|
|
1478
1581
|
}
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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);
|
|
1488
1609
|
continue;
|
|
1489
|
-
const categories = new Set;
|
|
1490
|
-
const definers = [];
|
|
1491
|
-
for (const { file, category } of byFile.values()) {
|
|
1492
|
-
categories.add(category);
|
|
1493
|
-
const { tag, reason } = classifyDefiner(file, name2, category, allowlist, readRaw);
|
|
1494
|
-
definers.push({
|
|
1495
|
-
file: file.path,
|
|
1496
|
-
package: file.packageName ?? "unknown",
|
|
1497
|
-
public: isFilePublic(file, name2),
|
|
1498
|
-
tag,
|
|
1499
|
-
...reason ? { reason } : {}
|
|
1500
|
-
});
|
|
1501
1610
|
}
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
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);
|
|
1506
1624
|
continue;
|
|
1507
1625
|
}
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
entryTag = dispatchCount >= 2 ? "DISPATCH_VARIANT" : "TRUE_DUPLICATE";
|
|
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;
|
|
1515
1632
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
function tallyByTag(entries) {
|
|
1522
|
-
const tally = {
|
|
1523
|
-
TRUE_DUPLICATE: 0,
|
|
1524
|
-
DISPATCH_VARIANT: 0,
|
|
1525
|
-
ALIAS_DELEGATION: 0,
|
|
1526
|
-
ALLOWLISTED: 0
|
|
1527
|
-
};
|
|
1528
|
-
for (const e of entries)
|
|
1529
|
-
tally[e.tag]++;
|
|
1530
|
-
return tally;
|
|
1531
|
-
}
|
|
1532
|
-
function detectDuplicateSymbols(files, publicSurface, root, allowlistPath = join4(outputDirOf(root), "duplicate-allowlist.json")) {
|
|
1533
|
-
const allowlist = loadDuplicateAllowlist(allowlistPath);
|
|
1534
|
-
const cache = new Map;
|
|
1535
|
-
const readRaw = (relPath) => {
|
|
1536
|
-
let content = cache.get(relPath);
|
|
1537
|
-
if (content === undefined) {
|
|
1538
|
-
try {
|
|
1539
|
-
content = readFileSync2(join4(root, relPath), "utf-8");
|
|
1540
|
-
} catch {
|
|
1541
|
-
content = "";
|
|
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>`);
|
|
1542
1637
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
return content;
|
|
1546
|
-
};
|
|
1547
|
-
return {
|
|
1548
|
-
runtime: buildDuplicateEntries(collectOwnDefiners(files, RUNTIME_DUP_CATEGORIES), publicSurface, allowlist, readRaw),
|
|
1549
|
-
types: buildDuplicateEntries(collectOwnDefiners(files, TYPE_DUP_CATEGORIES), publicSurface, allowlist, readRaw)
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
function buildDuplicateReport(dup) {
|
|
1553
|
-
const runtimeByTag = tallyByTag(dup.runtime);
|
|
1554
|
-
const typeByTag = tallyByTag(dup.types);
|
|
1555
|
-
return {
|
|
1556
|
-
note: DUPLICATE_SYMBOLS_NOTE,
|
|
1557
|
-
summary: {
|
|
1558
|
-
runtimeDuplicates: runtimeByTag.TRUE_DUPLICATE,
|
|
1559
|
-
typeDuplicates: typeByTag.TRUE_DUPLICATE,
|
|
1560
|
-
runtimeByTag,
|
|
1561
|
-
typeByTag
|
|
1562
|
-
},
|
|
1563
|
-
runtime: dup.runtime,
|
|
1564
|
-
types: dup.types
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
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`.";
|
|
1568
|
-
function trueDuplicateNames(entries) {
|
|
1569
|
-
const names = {};
|
|
1570
|
-
const found = entries.filter((e) => e.tag === "TRUE_DUPLICATE").sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
1571
|
-
for (const e of found)
|
|
1572
|
-
names[e.name] = e.definers.map((d) => d.file).sort(compareCodeUnits);
|
|
1573
|
-
return names;
|
|
1574
|
-
}
|
|
1575
|
-
function buildDuplicateBaseline(report) {
|
|
1576
|
-
return {
|
|
1577
|
-
note: DUPLICATE_BASELINE_NOTE,
|
|
1578
|
-
runtime: trueDuplicateNames(report.runtime),
|
|
1579
|
-
types: trueDuplicateNames(report.types)
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
function findNewDuplicates(report, baseline) {
|
|
1583
|
-
const found = [];
|
|
1584
|
-
for (const kind of ["runtime", "types"]) {
|
|
1585
|
-
const held = baseline[kind];
|
|
1586
|
-
for (const [name2, files] of Object.entries(trueDuplicateNames(report[kind]))) {
|
|
1587
|
-
if (!Object.hasOwn(held, name2))
|
|
1588
|
-
found.push({ kind, name: name2, files });
|
|
1638
|
+
apply(options, value, setRoot);
|
|
1639
|
+
continue;
|
|
1589
1640
|
}
|
|
1641
|
+
throw new Error(`unknown flag '${name2}' (see repo-tools ${command ?? "depgraph"} --help)`);
|
|
1590
1642
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
function isRecord(value) {
|
|
1594
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1643
|
+
checkModes(options);
|
|
1644
|
+
return options;
|
|
1595
1645
|
}
|
|
1596
|
-
function
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
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`);
|
|
1599
1657
|
}
|
|
1600
|
-
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");
|
|
1601
|
-
if (!parsed.runtime.every(valid) || !parsed.types.every(valid))
|
|
1602
|
-
return null;
|
|
1603
|
-
return parsed;
|
|
1604
1658
|
}
|
|
1605
|
-
function
|
|
1606
|
-
|
|
1607
|
-
return null;
|
|
1608
|
-
const kind = (value) => value === undefined ? {} : isRecord(value) ? value : null;
|
|
1609
|
-
const runtime = kind(parsed.runtime);
|
|
1610
|
-
const types = kind(parsed.types);
|
|
1611
|
-
return runtime && types ? { runtime, types } : null;
|
|
1659
|
+
function wantsHelp(argv) {
|
|
1660
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
1612
1661
|
}
|
|
1613
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
|
+
|
|
1614
1667
|
// src/depgraph/extensions.ts
|
|
1615
|
-
import { existsSync as existsSync3,
|
|
1616
|
-
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";
|
|
1617
1670
|
import { pathToFileURL } from "node:url";
|
|
1618
1671
|
|
|
1619
1672
|
// src/io.ts
|
|
1620
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
1621
|
-
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";
|
|
1622
1675
|
function writeLf(path, text) {
|
|
1623
1676
|
mkdirSync(dirname(path), { recursive: true });
|
|
1624
1677
|
writeFileSync(path, text.replace(/\r\n?/g, `
|
|
@@ -1636,6 +1689,22 @@ function toJson(value) {
|
|
|
1636
1689
|
return `${JSON.stringify(value, null, 2)}
|
|
1637
1690
|
`;
|
|
1638
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
|
+
}
|
|
1639
1708
|
|
|
1640
1709
|
// src/depgraph/extensions.ts
|
|
1641
1710
|
var MASK = Object.freeze({ blankCommentsAndStrings, stripComments });
|
|
@@ -1716,27 +1785,11 @@ function outsideText(folder, path) {
|
|
|
1716
1785
|
const rel = relative2(folder, path);
|
|
1717
1786
|
return rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("../") || isAbsolute(rel);
|
|
1718
1787
|
}
|
|
1719
|
-
function realPath(path) {
|
|
1720
|
-
let existing = resolve2(path);
|
|
1721
|
-
const rest = [];
|
|
1722
|
-
while (!lstatSync2(existing, { throwIfNoEntry: false })) {
|
|
1723
|
-
const up = dirname2(existing);
|
|
1724
|
-
if (up === existing)
|
|
1725
|
-
return;
|
|
1726
|
-
rest.unshift(basename(existing));
|
|
1727
|
-
existing = up;
|
|
1728
|
-
}
|
|
1729
|
-
try {
|
|
1730
|
-
return join5(realpathSync.native(existing), ...rest);
|
|
1731
|
-
} catch {
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
1788
|
function outputPath(outputDir, relPath) {
|
|
1736
1789
|
if (typeof relPath !== "string" || relPath === "" || isAbsolutePath(relPath)) {
|
|
1737
1790
|
throw new Error("write: the path must be relative to the output folder");
|
|
1738
1791
|
}
|
|
1739
|
-
const path =
|
|
1792
|
+
const path = resolve3(outputDir, relPath);
|
|
1740
1793
|
if (outsideText(outputDir, path)) {
|
|
1741
1794
|
throw new Error("write: the path must stay in the output folder");
|
|
1742
1795
|
}
|
|
@@ -1749,8 +1802,8 @@ function outputPath(outputDir, relPath) {
|
|
|
1749
1802
|
}
|
|
1750
1803
|
|
|
1751
1804
|
// src/map/artifacts.ts
|
|
1752
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
1753
|
-
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";
|
|
1754
1807
|
|
|
1755
1808
|
// src/map/cycles.ts
|
|
1756
1809
|
function sccContaining(start2, eligible, edges, reverseEdges) {
|
|
@@ -1890,7 +1943,7 @@ function stronglyConnectedComponents(graph) {
|
|
|
1890
1943
|
}
|
|
1891
1944
|
|
|
1892
1945
|
// src/depgraph/resolver.ts
|
|
1893
|
-
import { dirname as
|
|
1946
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
1894
1947
|
function distToSrc(path) {
|
|
1895
1948
|
const m = /(^|\/)dist\/(?:src\/)?/.exec(path);
|
|
1896
1949
|
if (!m)
|
|
@@ -1901,7 +1954,7 @@ function distToSrc(path) {
|
|
|
1901
1954
|
return `${before}${m[1]}src/${path.slice(m.index + m[0].length)}`;
|
|
1902
1955
|
}
|
|
1903
1956
|
function resolveCandidates(fromPath, spec) {
|
|
1904
|
-
const base = toPosix(join6(
|
|
1957
|
+
const base = toPosix(join6(dirname2(fromPath), spec));
|
|
1905
1958
|
let candidates;
|
|
1906
1959
|
if (base.endsWith(".js")) {
|
|
1907
1960
|
const stem = base.slice(0, -3);
|
|
@@ -2035,7 +2088,7 @@ function filesInCycles(components) {
|
|
|
2035
2088
|
|
|
2036
2089
|
// src/depgraph/roots.ts
|
|
2037
2090
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
2038
|
-
import { dirname as
|
|
2091
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
2039
2092
|
function isJsonObject(value) {
|
|
2040
2093
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2041
2094
|
}
|
|
@@ -2118,7 +2171,7 @@ function seedTsconfigEntries(root, pkgDir, cfgRel, add) {
|
|
|
2118
2171
|
} catch {
|
|
2119
2172
|
return;
|
|
2120
2173
|
}
|
|
2121
|
-
const cfgDir =
|
|
2174
|
+
const cfgDir = dirname3(join7(pkgDir, cfgRel));
|
|
2122
2175
|
const seedPath = (p) => {
|
|
2123
2176
|
if (p.includes("*")) {
|
|
2124
2177
|
const base = join7(root, cfgDir, p.replace(/\/?\*.*$/, ""));
|
|
@@ -2312,7 +2365,7 @@ function htmlReferencedEntries(root) {
|
|
|
2312
2365
|
const src = m[1] ?? "";
|
|
2313
2366
|
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(src) || !/\.[cm]?[jt]sx?$/.test(src))
|
|
2314
2367
|
continue;
|
|
2315
|
-
const fromPage = toPosix(join7(
|
|
2368
|
+
const fromPage = toPosix(join7(dirname3(page), src)).replace(/^\.\//, "");
|
|
2316
2369
|
const candidates = src.startsWith("/") ? [src.slice(1), fromPage] : [fromPage];
|
|
2317
2370
|
const found = candidates.find((c) => existsSync4(join7(root, c)));
|
|
2318
2371
|
if (found !== undefined && !out2.includes(found))
|
|
@@ -2332,7 +2385,7 @@ function runtimeLaunchedEntries(root, parsed) {
|
|
|
2332
2385
|
continue;
|
|
2333
2386
|
}
|
|
2334
2387
|
for (const m of code.matchAll(/new\s+URL\(\s*['"`](\.[^'"`]*?\.m?js)['"`]\s*,\s*import\.meta\.url/g)) {
|
|
2335
|
-
const sibling = toPosix(join7(
|
|
2388
|
+
const sibling = toPosix(join7(dirname3(file.path), m[1] ?? ""));
|
|
2336
2389
|
const tsPath = sibling.replace(/\.m?js$/, ".ts");
|
|
2337
2390
|
if (known.has(tsPath))
|
|
2338
2391
|
out2.add(tsPath);
|
|
@@ -6198,7 +6251,7 @@ import { readFileSync as readFileSync5 } from "node:fs";
|
|
|
6198
6251
|
import { join as join9, posix as posix2 } from "node:path";
|
|
6199
6252
|
|
|
6200
6253
|
// src/depgraph/description.ts
|
|
6201
|
-
import { basename as basename2, dirname as
|
|
6254
|
+
import { basename as basename2, dirname as dirname4 } from "node:path";
|
|
6202
6255
|
var RULE_LINE = /^[=\-*~#_]{3,}$/;
|
|
6203
6256
|
function extractDescription(content) {
|
|
6204
6257
|
const jsdocMatch = content.match(/\/\*\*\s*\n([^*]*(?:\*(?!\/)[^*]*)*)\*\//);
|
|
@@ -6233,7 +6286,7 @@ function generateFallbackDescription(file) {
|
|
|
6233
6286
|
const fileName = withoutTsExtension(basename2(file.path));
|
|
6234
6287
|
if (fileName === "index") {
|
|
6235
6288
|
if (file.exports.reExported.length > 0) {
|
|
6236
|
-
const pkgName = file.packageName ||
|
|
6289
|
+
const pkgName = file.packageName || dirname4(file.path).split("/").pop() || "";
|
|
6237
6290
|
return `Package entry point for ${pkgName || "module"} (re-exports ${file.exports.reExported.length} symbols)`;
|
|
6238
6291
|
}
|
|
6239
6292
|
if (file.exports.named.length > 0) {
|
|
@@ -6447,6 +6500,26 @@ function isReparsePoint(path) {
|
|
|
6447
6500
|
return false;
|
|
6448
6501
|
}
|
|
6449
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
|
+
}
|
|
6450
6523
|
function entries(dir) {
|
|
6451
6524
|
try {
|
|
6452
6525
|
return readdirHook.entries(dir);
|
|
@@ -6668,8 +6741,8 @@ function discover(root, language = detectLanguage(root), skippedLinks) {
|
|
|
6668
6741
|
}
|
|
6669
6742
|
|
|
6670
6743
|
// src/map/graph.ts
|
|
6671
|
-
import { readFileSync as
|
|
6672
|
-
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";
|
|
6673
6746
|
|
|
6674
6747
|
// node_modules/smol-toml/dist/error.js
|
|
6675
6748
|
/*!
|
|
@@ -10977,6 +11050,20 @@ function parserFor(name2) {
|
|
|
10977
11050
|
return parser;
|
|
10978
11051
|
}
|
|
10979
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
|
+
|
|
10980
11067
|
// src/map/parsing.ts
|
|
10981
11068
|
function emptyModule() {
|
|
10982
11069
|
return {
|
|
@@ -11044,6 +11131,7 @@ function importClauseNames(clause) {
|
|
|
11044
11131
|
return names;
|
|
11045
11132
|
}
|
|
11046
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"));
|
|
11047
11135
|
function isTypeOnlyImport(stmt, clause) {
|
|
11048
11136
|
if (hasChild(stmt, "type"))
|
|
11049
11137
|
return true;
|
|
@@ -11052,10 +11140,7 @@ function isTypeOnlyImport(stmt, clause) {
|
|
|
11052
11140
|
const kids = clause.children.filter((c) => c.type !== ",");
|
|
11053
11141
|
if (kids.length !== 1 || kids[0]?.type !== "named_imports")
|
|
11054
11142
|
return false;
|
|
11055
|
-
|
|
11056
|
-
if (specifiers.length === 0)
|
|
11057
|
-
return false;
|
|
11058
|
-
return specifiers.every((s) => hasChild(s, "type"));
|
|
11143
|
+
return allInlineTyped(kids[0].children.filter((c) => c.type === "import_specifier"));
|
|
11059
11144
|
}
|
|
11060
11145
|
function defaultExportKindAndLocal(stmt) {
|
|
11061
11146
|
for (const c of stmt.children) {
|
|
@@ -11120,11 +11205,10 @@ function exportClauseSourceNames(clause) {
|
|
|
11120
11205
|
function isTypeOnlyExport(stmt, clause) {
|
|
11121
11206
|
if (hasChild(stmt, "type"))
|
|
11122
11207
|
return true;
|
|
11123
|
-
|
|
11124
|
-
if (specifiers.length === 0)
|
|
11125
|
-
return false;
|
|
11126
|
-
return specifiers.every((s) => hasChild(s, "type"));
|
|
11208
|
+
return allInlineTyped(clause.children.filter((c) => c.type === "export_specifier"));
|
|
11127
11209
|
}
|
|
11210
|
+
var GAP = String.raw`(?:\s|/\*[\s\S]*?\*/)`;
|
|
11211
|
+
var EXPORT_TYPE_STAR = new RegExp(String.raw`^export${GAP}+type${GAP}*\*`, "u");
|
|
11128
11212
|
function stripChars(text, chars) {
|
|
11129
11213
|
let a = 0;
|
|
11130
11214
|
let b = text.length;
|
|
@@ -11243,6 +11327,7 @@ function parseTs(source, path = "") {
|
|
|
11243
11327
|
const nsExport = node.children.find((c) => c.type === "namespace_export");
|
|
11244
11328
|
let names = [];
|
|
11245
11329
|
let typeOnly = false;
|
|
11330
|
+
const typeStar = hasChild(node, "type") || EXPORT_TYPE_STAR.test(node.text);
|
|
11246
11331
|
if (clause) {
|
|
11247
11332
|
names = exportClauseSourceNames(clause);
|
|
11248
11333
|
typeOnly = isTypeOnlyExport(node, clause);
|
|
@@ -11250,11 +11335,12 @@ function parseTs(source, path = "") {
|
|
|
11250
11335
|
mod.reExports.push(name2);
|
|
11251
11336
|
} else if (nsExport) {
|
|
11252
11337
|
names = ["*"];
|
|
11338
|
+
typeOnly = typeStar;
|
|
11253
11339
|
for (const [name2] of exportEntries(node))
|
|
11254
11340
|
mod.reExports.push(name2);
|
|
11255
11341
|
} else {
|
|
11256
|
-
|
|
11257
|
-
mod.reExports.push(`${typeStar}* from ${specifierOf(spec)}`);
|
|
11342
|
+
typeOnly = typeStar;
|
|
11343
|
+
mod.reExports.push(`${typeStar ? "type " : ""}* from ${specifierOf(spec)}`);
|
|
11258
11344
|
}
|
|
11259
11345
|
mod.imports.push({ specifier: specifierOf(spec), names, typeOnly, reExport: true });
|
|
11260
11346
|
}
|
|
@@ -12362,7 +12448,7 @@ var STRING_LITERAL = /['"]([^'"\n]*)['"]/g;
|
|
|
12362
12448
|
var PATH_SEGMENT = new RegExp(`^(?:${W}|[.-])+$`, "u");
|
|
12363
12449
|
var lstripDotSlash = (text) => text.replace(/^[./]+/, "");
|
|
12364
12450
|
function launcherEntry(root, spec, known) {
|
|
12365
|
-
const path =
|
|
12451
|
+
const path = join12(root, lstripDotSlash(spec.replaceAll("\\", "/")));
|
|
12366
12452
|
let text;
|
|
12367
12453
|
try {
|
|
12368
12454
|
const st = statSync4(path);
|
|
@@ -12410,7 +12496,7 @@ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
12410
12496
|
function packageJsonRoots(root, known) {
|
|
12411
12497
|
let pkg;
|
|
12412
12498
|
try {
|
|
12413
|
-
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"))));
|
|
12414
12500
|
} catch {
|
|
12415
12501
|
return [[], []];
|
|
12416
12502
|
}
|
|
@@ -12458,7 +12544,7 @@ var PYTHON_FALLBACK_ROOTS = [
|
|
|
12458
12544
|
];
|
|
12459
12545
|
function pythonRoots(root, known) {
|
|
12460
12546
|
const warnings = [];
|
|
12461
|
-
const pyproject =
|
|
12547
|
+
const pyproject = join12(root, "pyproject.toml");
|
|
12462
12548
|
let isFile = false;
|
|
12463
12549
|
try {
|
|
12464
12550
|
isFile = statSync4(pyproject).isFile();
|
|
@@ -12509,31 +12595,11 @@ function pythonRoots(root, known) {
|
|
|
12509
12595
|
}
|
|
12510
12596
|
var CSHARP_MAIN = new RegExp(`${B}static${S}+(?:async${S}+)?[^${SPACE_BODY}(){};]+${S}+Main${S}*\\(`, "u");
|
|
12511
12597
|
var CSPROJ_EXE = new RegExp(`<OutputType>${S}*(Exe|WinExe)${S}*</OutputType>`, "iu");
|
|
12512
|
-
function rglobParts(root, suffix) {
|
|
12513
|
-
const found = [];
|
|
12514
|
-
const go = (dir, parts2) => {
|
|
12515
|
-
let entries;
|
|
12516
|
-
try {
|
|
12517
|
-
entries = readdirHook.entries(dir);
|
|
12518
|
-
} catch {
|
|
12519
|
-
return;
|
|
12520
|
-
}
|
|
12521
|
-
for (const e of entries) {
|
|
12522
|
-
const full = join11(dir, e.name);
|
|
12523
|
-
if (e.isDirectory() && !isReparsePoint(full))
|
|
12524
|
-
go(full, [...parts2, e.name]);
|
|
12525
|
-
else if (e.name.endsWith(suffix))
|
|
12526
|
-
found.push([...parts2, e.name]);
|
|
12527
|
-
}
|
|
12528
|
-
};
|
|
12529
|
-
go(root, []);
|
|
12530
|
-
return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
|
|
12531
|
-
}
|
|
12532
12598
|
function csharpRoots(root, known) {
|
|
12533
12599
|
const warnings = [];
|
|
12534
12600
|
const declaresMain = (rel) => {
|
|
12535
12601
|
try {
|
|
12536
|
-
return CSHARP_MAIN.test(readSource(
|
|
12602
|
+
return CSHARP_MAIN.test(readSource(join12(root, rel)));
|
|
12537
12603
|
} catch {
|
|
12538
12604
|
return false;
|
|
12539
12605
|
}
|
|
@@ -12544,7 +12610,7 @@ function csharpRoots(root, known) {
|
|
|
12544
12610
|
continue;
|
|
12545
12611
|
let text;
|
|
12546
12612
|
try {
|
|
12547
|
-
text = readSource(
|
|
12613
|
+
text = readSource(join12(root, ...parts2));
|
|
12548
12614
|
} catch (error) {
|
|
12549
12615
|
warnings.push(`could not read ${parts2.join("/")} (${error instanceof Error ? error.message : String(error)})`);
|
|
12550
12616
|
continue;
|
|
@@ -12566,7 +12632,7 @@ function csharpRoots(root, known) {
|
|
|
12566
12632
|
if (program.length > 0)
|
|
12567
12633
|
roots.push(program[0]);
|
|
12568
12634
|
else {
|
|
12569
|
-
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");
|
|
12570
12636
|
}
|
|
12571
12637
|
}
|
|
12572
12638
|
if (roots.length > 0)
|
|
@@ -12602,7 +12668,7 @@ function workspaceRoots(root, known) {
|
|
|
12602
12668
|
continue;
|
|
12603
12669
|
const prefix = `${ws.directory}/`;
|
|
12604
12670
|
const local = new Set([...known].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)));
|
|
12605
|
-
const [found, pkgWarnings] = packageJsonRoots(
|
|
12671
|
+
const [found, pkgWarnings] = packageJsonRoots(join12(root, ws.directory), local);
|
|
12606
12672
|
const pkgRoots = found.map((p) => prefix + p);
|
|
12607
12673
|
for (const entry of ws.extraEntries)
|
|
12608
12674
|
if (known.has(entry))
|
|
@@ -12651,7 +12717,7 @@ async function buildGraph(root) {
|
|
|
12651
12717
|
const parsed = new Map;
|
|
12652
12718
|
const sources = new Map;
|
|
12653
12719
|
for (const f of found) {
|
|
12654
|
-
const text = readSource(
|
|
12720
|
+
const text = readSource(join12(root, f.path)).replace(/\r\n?/g, `
|
|
12655
12721
|
`);
|
|
12656
12722
|
sources.set(f.path, text);
|
|
12657
12723
|
parsed.set(f.path, read(text, f.path));
|
|
@@ -12781,7 +12847,7 @@ async function buildGraph(root) {
|
|
|
12781
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");
|
|
12782
12848
|
}
|
|
12783
12849
|
const graph = newRepoGraph({
|
|
12784
|
-
name: basename3(
|
|
12850
|
+
name: packageField(root, "name") ?? basename3(resolve4(root)),
|
|
12785
12851
|
files,
|
|
12786
12852
|
roots,
|
|
12787
12853
|
warnings,
|
|
@@ -12885,7 +12951,7 @@ function pyJsonDumps(data) {
|
|
|
12885
12951
|
return out2;
|
|
12886
12952
|
}
|
|
12887
12953
|
function writeJson(path, data) {
|
|
12888
|
-
mkdirSync2(
|
|
12954
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
12889
12955
|
writeFileSync2(path, `${pyJsonDumps(data)}
|
|
12890
12956
|
`);
|
|
12891
12957
|
return path;
|
|
@@ -12968,11 +13034,11 @@ function emitDependencyGraph(graph, outDir, options = {}) {
|
|
|
12968
13034
|
...graph.language === "typescript" ? exportKindStats(graph) : {}
|
|
12969
13035
|
});
|
|
12970
13036
|
data.warnings = [...graph.warnings];
|
|
12971
|
-
return writeJson(
|
|
13037
|
+
return writeJson(join13(outDir, "dependency-graph.json"), data);
|
|
12972
13038
|
}
|
|
12973
13039
|
function readJson(path) {
|
|
12974
13040
|
try {
|
|
12975
|
-
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
13041
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFileSync10(path)));
|
|
12976
13042
|
} catch {
|
|
12977
13043
|
return null;
|
|
12978
13044
|
}
|
|
@@ -12982,7 +13048,7 @@ var PNPM_ITEM = new RegExp(`(?<=^|\\n)${S}*-${S}*['"]?([^'"${SPACE_BODY}#]+)['"]
|
|
|
12982
13048
|
function pnpmWorkspacePatterns(root) {
|
|
12983
13049
|
let text;
|
|
12984
13050
|
try {
|
|
12985
|
-
text = readSource(
|
|
13051
|
+
text = readSource(join13(root, "pnpm-workspace.yaml")).replace(/\r\n?/g, `
|
|
12986
13052
|
`);
|
|
12987
13053
|
} catch {
|
|
12988
13054
|
return [];
|
|
@@ -13012,29 +13078,9 @@ function normalizeWorkspacePatterns(raw) {
|
|
|
13012
13078
|
]
|
|
13013
13079
|
];
|
|
13014
13080
|
}
|
|
13015
|
-
function csprojParts(root) {
|
|
13016
|
-
const found = [];
|
|
13017
|
-
const go = (dir, parts2) => {
|
|
13018
|
-
let entries;
|
|
13019
|
-
try {
|
|
13020
|
-
entries = readdirHook.entries(dir);
|
|
13021
|
-
} catch {
|
|
13022
|
-
return;
|
|
13023
|
-
}
|
|
13024
|
-
for (const e of entries) {
|
|
13025
|
-
const full = join12(dir, e.name);
|
|
13026
|
-
if (e.isDirectory() && !isReparsePoint(full))
|
|
13027
|
-
go(full, [...parts2, e.name]);
|
|
13028
|
-
else if (e.name.endsWith(".csproj"))
|
|
13029
|
-
found.push([...parts2, e.name]);
|
|
13030
|
-
}
|
|
13031
|
-
};
|
|
13032
|
-
go(root, []);
|
|
13033
|
-
return found.sort((a, b) => compareCodeUnits(a.join("/"), b.join("/")));
|
|
13034
|
-
}
|
|
13035
13081
|
function csprojMap(root) {
|
|
13036
13082
|
const projects = new Map;
|
|
13037
|
-
for (const parts2 of
|
|
13083
|
+
for (const parts2 of rglobParts(root, ".csproj")) {
|
|
13038
13084
|
if (parts2.some((p) => p === "obj" || p === "bin"))
|
|
13039
13085
|
continue;
|
|
13040
13086
|
const name2 = parts2.at(-1);
|
|
@@ -13052,13 +13098,13 @@ function csprojMap(root) {
|
|
|
13052
13098
|
}
|
|
13053
13099
|
function subdirs(dir) {
|
|
13054
13100
|
try {
|
|
13055
|
-
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);
|
|
13056
13102
|
} catch {
|
|
13057
13103
|
return [];
|
|
13058
13104
|
}
|
|
13059
13105
|
}
|
|
13060
13106
|
function workspaceMap(root) {
|
|
13061
|
-
const pkg = readJson(
|
|
13107
|
+
const pkg = readJson(join13(root, "package.json"));
|
|
13062
13108
|
if (!isObject3(pkg)) {
|
|
13063
13109
|
return [
|
|
13064
13110
|
new Map,
|
|
@@ -13092,18 +13138,18 @@ function workspaceMap(root) {
|
|
|
13092
13138
|
const parent = pattern.slice(0, -2);
|
|
13093
13139
|
let isDir = false;
|
|
13094
13140
|
try {
|
|
13095
|
-
isDir = statSync5(
|
|
13141
|
+
isDir = statSync5(join13(root, parent)).isDirectory();
|
|
13096
13142
|
} catch {
|
|
13097
13143
|
isDir = false;
|
|
13098
13144
|
}
|
|
13099
13145
|
if (!isDir)
|
|
13100
13146
|
continue;
|
|
13101
|
-
candidates = subdirs(
|
|
13147
|
+
candidates = subdirs(join13(root, parent)).map((d) => parent ? `${parent}/${d}` : d);
|
|
13102
13148
|
} else {
|
|
13103
13149
|
candidates = [pattern];
|
|
13104
13150
|
}
|
|
13105
13151
|
for (const relCand of candidates) {
|
|
13106
|
-
const sub = readJson(
|
|
13152
|
+
const sub = readJson(join13(root, relCand, "package.json"));
|
|
13107
13153
|
if (!isObject3(sub)) {
|
|
13108
13154
|
warnings.push(`emit_file_inventory: workspace candidate '${relCand}' (matched by '${pattern}') has no readable package.json -- skipped, its files will report package '(root)'`);
|
|
13109
13155
|
continue;
|
|
@@ -13163,7 +13209,7 @@ function emitFileInventory(graph, outDir) {
|
|
|
13163
13209
|
}));
|
|
13164
13210
|
const byDisposition = Object.fromEntries(DISPOSITIONS.map((d) => [d, 0]));
|
|
13165
13211
|
Object.assign(byDisposition, counter(files.map((f) => f.disposition)));
|
|
13166
|
-
return writeJson(
|
|
13212
|
+
return writeJson(join13(outDir, "file-inventory.json"), {
|
|
13167
13213
|
totalFiles: files.length,
|
|
13168
13214
|
byDisposition,
|
|
13169
13215
|
byArea: counter(files.map((f) => f.area)),
|
|
@@ -13197,7 +13243,7 @@ function readSourceText(rootPath, rel) {
|
|
|
13197
13243
|
if (rootPath === null)
|
|
13198
13244
|
return null;
|
|
13199
13245
|
try {
|
|
13200
|
-
return readSource(
|
|
13246
|
+
return readSource(join13(rootPath, rel)).replace(/\r\n?/g, `
|
|
13201
13247
|
`);
|
|
13202
13248
|
} catch {
|
|
13203
13249
|
return null;
|
|
@@ -13352,7 +13398,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
|
|
|
13352
13398
|
duplicates[sym] = paths;
|
|
13353
13399
|
}
|
|
13354
13400
|
const summary = { duplicateCount: Object.keys(duplicates).length, totalSymbols: owners.size };
|
|
13355
|
-
const path =
|
|
13401
|
+
const path = join13(outDir, "duplicate-symbols.json");
|
|
13356
13402
|
const root = graph.rootPath;
|
|
13357
13403
|
if (graph.language !== "typescript" || root === null) {
|
|
13358
13404
|
const reason = root === null ? "This graph has no root folder to read the definitions from." : `This repository reads as ${graph.language}.`;
|
|
@@ -13368,7 +13414,7 @@ function emitDuplicateSymbols(graph, outDir, options = {}) {
|
|
|
13368
13414
|
const rootEntries = workspaces.size > 0 ? [] : rootPackageEntries(root);
|
|
13369
13415
|
const surface = computePublicSurface(records, root, workspaces, rootEntries);
|
|
13370
13416
|
const definers = records.filter((r) => !/\.d\.[cm]?ts$/.test(r.path));
|
|
13371
|
-
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);
|
|
13372
13418
|
return writeJson(path, {
|
|
13373
13419
|
note: DUPLICATES_CLASSIFIED_NOTE,
|
|
13374
13420
|
classificationNote: report.note,
|
|
@@ -13384,7 +13430,7 @@ function emitUnusedAnalysis(graph, outDir) {
|
|
|
13384
13430
|
const [unreferenced, referencedInModule, unclassified] = classifyUnusedExports(graph);
|
|
13385
13431
|
const notes = unreferencedAnywhereNotes(graph, unreferenced);
|
|
13386
13432
|
const noImporter = noImporterFiles(graph);
|
|
13387
|
-
return writeJson(
|
|
13433
|
+
return writeJson(join13(outDir, "unused-analysis.json"), {
|
|
13388
13434
|
caveats: caveatsFor(graph),
|
|
13389
13435
|
summary: {
|
|
13390
13436
|
unusedExportCount: countAll(unreferenced) + countAll(referencedInModule) + countAll(unclassified),
|
|
@@ -13402,16 +13448,16 @@ function emitUnusedAnalysis(graph, outDir) {
|
|
|
13402
13448
|
}
|
|
13403
13449
|
|
|
13404
13450
|
// src/map/coverage.ts
|
|
13405
|
-
import { join as
|
|
13451
|
+
import { join as join17 } from "node:path";
|
|
13406
13452
|
|
|
13407
13453
|
// src/depgraph/coverage.ts
|
|
13408
|
-
import { existsSync as existsSync6, readFileSync as
|
|
13409
|
-
import { join as
|
|
13454
|
+
import { existsSync as existsSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
13455
|
+
import { join as join14 } from "node:path";
|
|
13410
13456
|
function loadCoveragePolicy(policyPath) {
|
|
13411
13457
|
if (!existsSync6(policyPath))
|
|
13412
13458
|
return null;
|
|
13413
13459
|
try {
|
|
13414
|
-
const parsed = JSON.parse(
|
|
13460
|
+
const parsed = JSON.parse(readFileSync11(policyPath, "utf8"));
|
|
13415
13461
|
if (!parsed.categories || typeof parsed.categories !== "object")
|
|
13416
13462
|
return null;
|
|
13417
13463
|
return parsed;
|
|
@@ -13511,7 +13557,7 @@ function traceReExports(importedPath, reExportMap) {
|
|
|
13511
13557
|
result.add(source);
|
|
13512
13558
|
return result;
|
|
13513
13559
|
}
|
|
13514
|
-
function analyzeTestCoverage(graphFiles, testFiles, root, policyPath =
|
|
13560
|
+
function analyzeTestCoverage(graphFiles, testFiles, root, policyPath = join14(outputDirOf(root), "coverage-policy.json")) {
|
|
13515
13561
|
const sourceFiles = graphFiles.filter((f) => !f.path.endsWith(".d.ts"));
|
|
13516
13562
|
const sourceFilePaths = new Set(sourceFiles.map((f) => f.path));
|
|
13517
13563
|
const coverageMap = new Map;
|
|
@@ -13650,10 +13696,10 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
|
|
|
13650
13696
|
}
|
|
13651
13697
|
lines.push("---");
|
|
13652
13698
|
lines.push("");
|
|
13653
|
-
lines.push("## Source Files
|
|
13699
|
+
lines.push("## Source Files Not Imported By A Test");
|
|
13654
13700
|
lines.push("");
|
|
13655
13701
|
if (coverage.untestedFiles.length === 0) {
|
|
13656
|
-
lines.push("**
|
|
13702
|
+
lines.push("**Every source file is imported by a test file.** \uD83C\uDF89");
|
|
13657
13703
|
} else {
|
|
13658
13704
|
lines.push(`The following ${coverage.untestedFiles.length} source files are not directly imported by any test file:`);
|
|
13659
13705
|
lines.push("");
|
|
@@ -13677,7 +13723,7 @@ function generateTestCoverageMarkdown(coverage, options = {}) {
|
|
|
13677
13723
|
}
|
|
13678
13724
|
lines.push("---");
|
|
13679
13725
|
lines.push("");
|
|
13680
|
-
lines.push("## Source Files
|
|
13726
|
+
lines.push("## Source Files Imported By A Test");
|
|
13681
13727
|
lines.push("");
|
|
13682
13728
|
lines.push("| Source File | Test Files |");
|
|
13683
13729
|
lines.push("|-------------|------------|");
|
|
@@ -13741,7 +13787,7 @@ function generateTestCoverageJson(coverage, options = {}) {
|
|
|
13741
13787
|
|
|
13742
13788
|
// src/map/reports.ts
|
|
13743
13789
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
13744
|
-
import { join as
|
|
13790
|
+
import { join as join16 } from "node:path";
|
|
13745
13791
|
|
|
13746
13792
|
// src/depgraph/reporters/json.ts
|
|
13747
13793
|
function modulesJsonOf(modules) {
|
|
@@ -13958,10 +14004,14 @@ function fileSection(lines, path, file) {
|
|
|
13958
14004
|
lines.push("---");
|
|
13959
14005
|
lines.push("");
|
|
13960
14006
|
}
|
|
13961
|
-
|
|
14007
|
+
var DEPGRAPH_LABELS = {
|
|
14008
|
+
files: "Total TypeScript Files",
|
|
14009
|
+
modules: "Total Modules"
|
|
14010
|
+
};
|
|
14011
|
+
function summaryRowsOf(stats, labels = DEPGRAPH_LABELS) {
|
|
13962
14012
|
const rows = [
|
|
13963
|
-
[
|
|
13964
|
-
[
|
|
14013
|
+
[labels.files, stats.totalTypeScriptFiles],
|
|
14014
|
+
[labels.modules, stats.totalModules],
|
|
13965
14015
|
["Total Lines of Code", stats.totalLinesOfCode],
|
|
13966
14016
|
["Total Exports", stats.totalExports],
|
|
13967
14017
|
["Total Re-exports", stats.totalReExports],
|
|
@@ -14104,20 +14154,12 @@ function generateYaml(json, dumpFn = dump) {
|
|
|
14104
14154
|
}
|
|
14105
14155
|
|
|
14106
14156
|
// src/map/layers.ts
|
|
14107
|
-
import {
|
|
14108
|
-
import { join as join14 } from "node:path";
|
|
14157
|
+
import { join as join15 } from "node:path";
|
|
14109
14158
|
function projectIdentity(root, graph) {
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
|
|
14113
|
-
} catch {}
|
|
14114
|
-
const field = (key) => {
|
|
14115
|
-
if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
|
|
14116
|
-
return null;
|
|
14117
|
-
const value = pkg[key];
|
|
14118
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
14159
|
+
return {
|
|
14160
|
+
name: packageField(root, "name") ?? graph.name,
|
|
14161
|
+
version: packageField(root, "version") ?? "unknown"
|
|
14119
14162
|
};
|
|
14120
|
-
return { name: field("name") ?? graph.name, version: field("version") ?? "unknown" };
|
|
14121
14163
|
}
|
|
14122
14164
|
function subsystemView(graph, root, warn = () => {}) {
|
|
14123
14165
|
const records = toParsedFiles(graph, root);
|
|
@@ -14129,7 +14171,7 @@ function subsystemView(graph, root, warn = () => {}) {
|
|
|
14129
14171
|
}
|
|
14130
14172
|
function emitDependencyLayers(graph, root, outDir, view = subsystemView(graph, root)) {
|
|
14131
14173
|
const identity = projectIdentity(root, graph);
|
|
14132
|
-
return writeJson(
|
|
14174
|
+
return writeJson(join15(outDir, "dependency-layers.json"), {
|
|
14133
14175
|
metadata: {
|
|
14134
14176
|
name: identity.name,
|
|
14135
14177
|
version: identity.version,
|
|
@@ -14168,33 +14210,14 @@ function statisticsFromCore(core, subsystems) {
|
|
|
14168
14210
|
unusedExportsCount: num("unusedExportsCount")
|
|
14169
14211
|
};
|
|
14170
14212
|
}
|
|
14171
|
-
|
|
14172
|
-
const rows = [
|
|
14173
|
-
["Total Source Files", stats.totalTypeScriptFiles],
|
|
14174
|
-
["Subsystems", stats.totalModules],
|
|
14175
|
-
["Total Lines of Code", stats.totalLinesOfCode],
|
|
14176
|
-
["Total Exports", stats.totalExports],
|
|
14177
|
-
["Total Re-exports", stats.totalReExports],
|
|
14178
|
-
["Total Classes", stats.totalClasses],
|
|
14179
|
-
["Total Interfaces", stats.totalInterfaces],
|
|
14180
|
-
["Total Functions", stats.totalFunctions],
|
|
14181
|
-
["Total Type Guards", stats.totalTypeGuards],
|
|
14182
|
-
["Total Enums", stats.totalEnums],
|
|
14183
|
-
["Type-only Imports", stats.totalTypeOnlyImports],
|
|
14184
|
-
["Runtime Cyclic Components", stats.runtimeCyclicComponents],
|
|
14185
|
-
["Type-only Cyclic Components", stats.typeOnlyCyclicComponents],
|
|
14186
|
-
["Files in Runtime Cycles", stats.runtimeFilesInCycles],
|
|
14187
|
-
["Files in Type-only Cycles", stats.typeOnlyFilesInCycles]
|
|
14188
|
-
];
|
|
14189
|
-
return rows.filter((r) => r[1] !== undefined);
|
|
14190
|
-
}
|
|
14213
|
+
var SUMMARY_LABELS = { files: "Total Source Files", modules: "Subsystems" };
|
|
14191
14214
|
function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
|
|
14192
14215
|
const view = options.view ?? subsystemView(graph, root);
|
|
14193
14216
|
const core = JSON.parse(readFileSync12(corePath, "utf8"));
|
|
14194
14217
|
const stats = statisticsFromCore(core.statistics, Object.keys(view.modules).length);
|
|
14195
14218
|
const identity = projectIdentity(root, graph);
|
|
14196
14219
|
const banner = { command: MAP_REGENERATE_COMMAND, ...options.banner };
|
|
14197
|
-
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));
|
|
14198
14221
|
const written = [
|
|
14199
14222
|
["DEPENDENCY_GRAPH.md", withBanner(markdown, banner)],
|
|
14200
14223
|
["dependency-graph.yaml", generateYaml(core)],
|
|
@@ -14204,7 +14227,7 @@ function emitSubsystemReports(graph, root, outDir, corePath, options = {}) {
|
|
|
14204
14227
|
]
|
|
14205
14228
|
];
|
|
14206
14229
|
return written.map(([name2, text]) => {
|
|
14207
|
-
const path =
|
|
14230
|
+
const path = join16(outDir, name2);
|
|
14208
14231
|
writeReport(path, text);
|
|
14209
14232
|
return path;
|
|
14210
14233
|
});
|
|
@@ -14222,9 +14245,9 @@ function isTestFile(path, language, area) {
|
|
|
14222
14245
|
function emitTestCoverage(graph, root, outDir, options = {}) {
|
|
14223
14246
|
const sources = toParsedFiles(graph, root);
|
|
14224
14247
|
const tests = toParsedFiles(graph, root, { allAreas: true, loadEdges: true }).filter((r) => isTestFile(r.path, graph.language, graph.files.get(r.path)?.area ?? ""));
|
|
14225
|
-
const coverage = analyzeTestCoverage(sources, tests, root, options.policyPath ??
|
|
14226
|
-
const mdPath =
|
|
14227
|
-
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");
|
|
14228
14251
|
const namespaceImportBasis = !importsNameSymbols(graph.language);
|
|
14229
14252
|
const markdown = generateTestCoverageMarkdown(coverage, {
|
|
14230
14253
|
expectedTestHint: graph.language === "typescript" && tests.some((t) => t.path.startsWith("tests/unit/")),
|
|
@@ -14238,7 +14261,7 @@ function emitTestCoverage(graph, root, outDir, options = {}) {
|
|
|
14238
14261
|
|
|
14239
14262
|
// src/map/markdown.ts
|
|
14240
14263
|
import { readFileSync as readFileSync13 } from "node:fs";
|
|
14241
|
-
import { join as
|
|
14264
|
+
import { join as join18 } from "node:path";
|
|
14242
14265
|
|
|
14243
14266
|
// src/depgraph/reporters/duplicates.ts
|
|
14244
14267
|
function summaryTable(byTag, total) {
|
|
@@ -14278,7 +14301,7 @@ function renderTaggedSection(title, entries, tag) {
|
|
|
14278
14301
|
|
|
14279
14302
|
${renderTable(entries.filter((e) => e.tag === tag))}`;
|
|
14280
14303
|
}
|
|
14281
|
-
function generateDuplicateSymbolsMarkdown(report) {
|
|
14304
|
+
function generateDuplicateSymbolsMarkdown(report, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
14282
14305
|
let md = `# Duplicate Symbols
|
|
14283
14306
|
|
|
14284
14307
|
`;
|
|
@@ -14286,9 +14309,14 @@ function generateDuplicateSymbolsMarkdown(report) {
|
|
|
14286
14309
|
md += "A file that only re-exports the name does not count. ";
|
|
14287
14310
|
md += "Each name carries one tag (see `DupEntryTag`), so the actionable subset is clear:\n\n";
|
|
14288
14311
|
md += "- `TRUE_DUPLICATE`: a real merge target.\n";
|
|
14289
|
-
|
|
14290
|
-
|
|
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.
|
|
14291
14318
|
`;
|
|
14319
|
+
}
|
|
14292
14320
|
md += "- `ALIAS_DELEGATION`: a `const X = importedY` forward. ";
|
|
14293
14321
|
md += `The entry is excluded once fewer than two real bodies remain.
|
|
14294
14322
|
`;
|
|
@@ -14383,7 +14411,7 @@ function inventoryMarkdown(inv, language) {
|
|
|
14383
14411
|
return lines.join(`
|
|
14384
14412
|
`);
|
|
14385
14413
|
}
|
|
14386
|
-
function duplicatesMarkdown(dup) {
|
|
14414
|
+
function duplicatesMarkdown(dup, dispatchCalls = DEFAULT_DISPATCH_CALLS) {
|
|
14387
14415
|
if (Array.isArray(dup.runtime) && Array.isArray(dup.types)) {
|
|
14388
14416
|
const report = {
|
|
14389
14417
|
note: dup.classificationNote,
|
|
@@ -14396,7 +14424,7 @@ function duplicatesMarkdown(dup) {
|
|
|
14396
14424
|
runtime: dup.runtime,
|
|
14397
14425
|
types: dup.types
|
|
14398
14426
|
};
|
|
14399
|
-
return generateDuplicateSymbolsMarkdown(report);
|
|
14427
|
+
return generateDuplicateSymbolsMarkdown(report, dispatchCalls);
|
|
14400
14428
|
}
|
|
14401
14429
|
const lines = ["# Duplicate Symbols", ""];
|
|
14402
14430
|
lines.push(`> **Note:** ${dup.note}`, "", `> ${dup.classificationNote}`, "");
|
|
@@ -14467,17 +14495,17 @@ function unusedMarkdown(unused, graph) {
|
|
|
14467
14495
|
return lines.join(`
|
|
14468
14496
|
`);
|
|
14469
14497
|
}
|
|
14470
|
-
function emitMarkdownReports(outDir, banner = {}) {
|
|
14471
|
-
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"));
|
|
14472
14500
|
const graph = read("dependency-graph.json");
|
|
14473
14501
|
const bannerOptions = { command: MAP_REGENERATE_COMMAND, ...banner };
|
|
14474
14502
|
const reports = [
|
|
14475
14503
|
["FILE_INVENTORY.md", inventoryMarkdown(read("file-inventory.json"), graph.metadata.language)],
|
|
14476
|
-
["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"))],
|
|
14504
|
+
["duplicate-symbols.md", duplicatesMarkdown(read("duplicate-symbols.json"), dispatchCalls)],
|
|
14477
14505
|
["unused-analysis.md", unusedMarkdown(read("unused-analysis.json"), graph)]
|
|
14478
14506
|
];
|
|
14479
14507
|
return reports.map(([name2, body2]) => {
|
|
14480
|
-
const path =
|
|
14508
|
+
const path = join18(outDir, name2);
|
|
14481
14509
|
writeReport(path, withBanner(body2, bannerOptions));
|
|
14482
14510
|
return path;
|
|
14483
14511
|
});
|
|
@@ -14485,7 +14513,7 @@ function emitMarkdownReports(outDir, banner = {}) {
|
|
|
14485
14513
|
|
|
14486
14514
|
// src/map/surfaces.ts
|
|
14487
14515
|
import { readFileSync as readFileSync14 } from "node:fs";
|
|
14488
|
-
import { join as
|
|
14516
|
+
import { join as join19 } from "node:path";
|
|
14489
14517
|
|
|
14490
14518
|
// src/depgraph/reporters/surfaces.ts
|
|
14491
14519
|
function packageKeyOf(moduleKey) {
|
|
@@ -14520,7 +14548,7 @@ function emitApiSurface(graph, root, outPath, entry, stabilityTags) {
|
|
|
14520
14548
|
if (graph.language === "typescript") {
|
|
14521
14549
|
const load = (p) => {
|
|
14522
14550
|
try {
|
|
14523
|
-
return readFileSync14(
|
|
14551
|
+
return readFileSync14(join19(root, p), "utf8");
|
|
14524
14552
|
} catch {
|
|
14525
14553
|
return null;
|
|
14526
14554
|
}
|
|
@@ -14566,7 +14594,7 @@ function sortedSurfaces(map) {
|
|
|
14566
14594
|
return out2;
|
|
14567
14595
|
}
|
|
14568
14596
|
function emitExportSurfaces(graph, root, outDir) {
|
|
14569
|
-
const path =
|
|
14597
|
+
const path = join19(outDir, "package-export-surfaces.json");
|
|
14570
14598
|
if (graph.language === "typescript") {
|
|
14571
14599
|
const records = toParsedFiles(graph, root);
|
|
14572
14600
|
const workspaces = detectWorkspaces(root);
|
|
@@ -14694,7 +14722,7 @@ async function runMap(argv, io, command = "map") {
|
|
|
14694
14722
|
`);
|
|
14695
14723
|
if (options.strictOrphans)
|
|
14696
14724
|
options.settings.strictOrphans = true;
|
|
14697
|
-
root =
|
|
14725
|
+
root = resolve5(options.root);
|
|
14698
14726
|
if (!isDirectory(root))
|
|
14699
14727
|
throw new Error("the root <root> is not an existing directory");
|
|
14700
14728
|
const fileSettings = loadConfigFile(root, options.config);
|
|
@@ -14761,13 +14789,14 @@ async function runPipeline(root, options, config, io) {
|
|
|
14761
14789
|
const corePath = emitDependencyGraph(graph, outDir);
|
|
14762
14790
|
written.push(corePath, emitFileInventory(graph, outDir));
|
|
14763
14791
|
written.push(emitDuplicateSymbols(graph, outDir, {
|
|
14764
|
-
allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist)
|
|
14792
|
+
allowlistPath: resolveUnderRoot(root, config.duplicateAllowlist),
|
|
14793
|
+
dispatchCalls: config.dispatchCalls
|
|
14765
14794
|
}));
|
|
14766
14795
|
written.push(emitUnusedAnalysis(graph, outDir));
|
|
14767
14796
|
const view = subsystemView(graph, root);
|
|
14768
14797
|
written.push(emitDependencyLayers(graph, root, outDir, view));
|
|
14769
14798
|
written.push(...emitSubsystemReports(graph, root, outDir, corePath, { view, banner }));
|
|
14770
|
-
written.push(...emitMarkdownReports(outDir, banner));
|
|
14799
|
+
written.push(...emitMarkdownReports(outDir, banner, config.dispatchCalls));
|
|
14771
14800
|
written.push(...emitTestCoverage(graph, root, outDir, {
|
|
14772
14801
|
policyPath: resolveUnderRoot(root, config.coveragePolicy),
|
|
14773
14802
|
policyNote: config.coveragePolicy.replace(/\\/g, "/"),
|
|
@@ -14971,7 +15000,7 @@ function parseClaims(markdown) {
|
|
|
14971
15000
|
// src/check/metrics.ts
|
|
14972
15001
|
import { mkdtempSync, readFileSync as readFileSync16, rmSync } from "node:fs";
|
|
14973
15002
|
import { tmpdir } from "node:os";
|
|
14974
|
-
import { join as
|
|
15003
|
+
import { join as join20 } from "node:path";
|
|
14975
15004
|
var readReport = (path) => JSON.parse(readFileSync16(path, "utf8"));
|
|
14976
15005
|
function section(report, key) {
|
|
14977
15006
|
const value = report[key];
|
|
@@ -14989,11 +15018,14 @@ function addScalars(into, source) {
|
|
|
14989
15018
|
}
|
|
14990
15019
|
async function collectMetrics(root, options = {}) {
|
|
14991
15020
|
const graph = await graphOf(root);
|
|
14992
|
-
const scratch = mkdtempSync(
|
|
15021
|
+
const scratch = mkdtempSync(join20(options.scratchBase ?? tmpdir(), "repo-tools-check-"));
|
|
14993
15022
|
try {
|
|
14994
15023
|
const dep = readReport(emitDependencyGraph(graph, scratch));
|
|
14995
15024
|
const inventory = readReport(emitFileInventory(graph, scratch));
|
|
14996
|
-
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
|
+
}));
|
|
14997
15029
|
const unused = readReport(emitUnusedAnalysis(graph, scratch));
|
|
14998
15030
|
const metrics = new Map;
|
|
14999
15031
|
addScalars(metrics, section(dep, "metadata"));
|
|
@@ -15191,7 +15223,7 @@ function readDocs(dir, label) {
|
|
|
15191
15223
|
return names.map((name2) => {
|
|
15192
15224
|
let bytes;
|
|
15193
15225
|
try {
|
|
15194
|
-
bytes = readFileSync17(
|
|
15226
|
+
bytes = readFileSync17(join21(dir, name2));
|
|
15195
15227
|
} catch {
|
|
15196
15228
|
throw new Error(`cannot read ${label}/${name2}`);
|
|
15197
15229
|
}
|
|
@@ -15218,16 +15250,17 @@ async function run2(argv, io) {
|
|
|
15218
15250
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
15219
15251
|
try {
|
|
15220
15252
|
const options = parseCheckArgs(argv, process.cwd());
|
|
15221
|
-
root =
|
|
15253
|
+
root = resolve6(options.root);
|
|
15222
15254
|
if (!isDirectory(root))
|
|
15223
15255
|
throw new Error("the root <root> is not an existing directory");
|
|
15224
15256
|
if (options.docs === undefined)
|
|
15225
15257
|
throw new Error("flag --docs is required: --docs=<dir>");
|
|
15226
15258
|
const config = mergeMapConfig({}, loadConfigFile(root, options.config));
|
|
15227
15259
|
const label = isAbsolutePath(options.docs) ? "<docs>" : `<root>/${options.docs.replace(/\\/g, "/")}`;
|
|
15228
|
-
const docs = readDocs(
|
|
15260
|
+
const docs = readDocs(resolve6(root, options.docs), label);
|
|
15229
15261
|
const measured = await collectMetrics(root, {
|
|
15230
|
-
allowlistPath:
|
|
15262
|
+
allowlistPath: resolve6(root, config.duplicateAllowlist),
|
|
15263
|
+
dispatchCalls: config.dispatchCalls
|
|
15231
15264
|
});
|
|
15232
15265
|
const markers = [NO_VERIFICATION_MARKER];
|
|
15233
15266
|
if (config.verificationMarker !== null && !markers.includes(config.verificationMarker)) {
|
|
@@ -15259,12 +15292,12 @@ import {
|
|
|
15259
15292
|
statSync as statSync7,
|
|
15260
15293
|
writeFileSync as writeFileSync3
|
|
15261
15294
|
} from "node:fs";
|
|
15262
|
-
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";
|
|
15263
15296
|
|
|
15264
15297
|
// src/chunk/manifest.ts
|
|
15265
15298
|
import { createHash } from "node:crypto";
|
|
15266
15299
|
import { readFileSync as readFileSync18 } from "node:fs";
|
|
15267
|
-
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";
|
|
15268
15301
|
|
|
15269
15302
|
// src/chunk/splitters.ts
|
|
15270
15303
|
import { extname } from "node:path";
|
|
@@ -15895,7 +15928,7 @@ function relativeSource(manifestDir, sourcePath) {
|
|
|
15895
15928
|
return relative3(manifestDir, sourcePath).split(sep2).join("/");
|
|
15896
15929
|
}
|
|
15897
15930
|
function resolveSource(manifestDir, sourceFile) {
|
|
15898
|
-
return
|
|
15931
|
+
return resolve7(manifestDir, sourceFile);
|
|
15899
15932
|
}
|
|
15900
15933
|
function readManifest(path) {
|
|
15901
15934
|
return validateManifest(JSON.parse(readFileSync18(path, "utf8")));
|
|
@@ -16114,34 +16147,18 @@ function isOutsideText(folder, path) {
|
|
|
16114
16147
|
const rel = relative4(folder, path);
|
|
16115
16148
|
return rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel);
|
|
16116
16149
|
}
|
|
16117
|
-
function realPath2(path) {
|
|
16118
|
-
let existing = resolve7(path);
|
|
16119
|
-
const rest = [];
|
|
16120
|
-
while (!lstatSync4(existing, { throwIfNoEntry: false })) {
|
|
16121
|
-
const up = dirname7(existing);
|
|
16122
|
-
if (up === existing)
|
|
16123
|
-
return;
|
|
16124
|
-
rest.unshift(basename7(existing));
|
|
16125
|
-
existing = up;
|
|
16126
|
-
}
|
|
16127
|
-
try {
|
|
16128
|
-
return join21(realpathSync2.native(existing), ...rest);
|
|
16129
|
-
} catch {
|
|
16130
|
-
return;
|
|
16131
|
-
}
|
|
16132
|
-
}
|
|
16133
16150
|
function isOutside(folder, path) {
|
|
16134
16151
|
if (isOutsideText(folder, path))
|
|
16135
16152
|
return true;
|
|
16136
|
-
const realFolder =
|
|
16137
|
-
const realTarget =
|
|
16153
|
+
const realFolder = realPath(folder);
|
|
16154
|
+
const realTarget = realPath(path);
|
|
16138
16155
|
return realFolder === undefined || realTarget === undefined || isOutsideText(realFolder, realTarget);
|
|
16139
16156
|
}
|
|
16140
16157
|
function isInside(folder, path) {
|
|
16141
16158
|
if (!isOutsideText(folder, path))
|
|
16142
16159
|
return true;
|
|
16143
|
-
const realFolder =
|
|
16144
|
-
const realTarget =
|
|
16160
|
+
const realFolder = realPath(folder);
|
|
16161
|
+
const realTarget = realPath(path);
|
|
16145
16162
|
return realFolder !== undefined && realTarget !== undefined && !isOutsideText(realFolder, realTarget);
|
|
16146
16163
|
}
|
|
16147
16164
|
function chunkFolderError(chunksDir, paths) {
|
|
@@ -16154,11 +16171,11 @@ function chunkFolderError(chunksDir, paths) {
|
|
|
16154
16171
|
return;
|
|
16155
16172
|
}
|
|
16156
16173
|
function outsideError(sourcePath, chunksDir, remedy) {
|
|
16157
|
-
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}
|
|
16158
16175
|
`;
|
|
16159
16176
|
}
|
|
16160
16177
|
function readChunk(chunksDir, filename) {
|
|
16161
|
-
const chunkPath =
|
|
16178
|
+
const chunkPath = join22(chunksDir, filename);
|
|
16162
16179
|
const stat = lstatSync4(chunkPath, { throwIfNoEntry: false });
|
|
16163
16180
|
if (!stat)
|
|
16164
16181
|
return;
|
|
@@ -16192,7 +16209,7 @@ function split(inputFile, options, io) {
|
|
|
16192
16209
|
const log = lineWriter(io);
|
|
16193
16210
|
const maxLines = options.maxLines ?? 500;
|
|
16194
16211
|
const dryRun = options.dryRun ?? false;
|
|
16195
|
-
const absoluteInput =
|
|
16212
|
+
const absoluteInput = resolve8(inputFile);
|
|
16196
16213
|
if (!existsSync8(absoluteInput)) {
|
|
16197
16214
|
io.stderr(`Error: File not found: ${absoluteInput}
|
|
16198
16215
|
`);
|
|
@@ -16209,7 +16226,7 @@ function split(inputFile, options, io) {
|
|
|
16209
16226
|
const content = readFileSync19(absoluteInput, "utf8");
|
|
16210
16227
|
const sourceHash = contentHash(content);
|
|
16211
16228
|
const baseName = basename7(inputFile, extname2(inputFile));
|
|
16212
|
-
const outputDir = options.output ?
|
|
16229
|
+
const outputDir = options.output ? resolve8(options.output) : join22(dirname6(absoluteInput), `${baseName}_chunks`);
|
|
16213
16230
|
const sourceFile = relativeSource(outputDir, absoluteInput);
|
|
16214
16231
|
if (isAbsoluteAnywhere(sourceFile)) {
|
|
16215
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.
|
|
@@ -16251,7 +16268,7 @@ chunker - Splitting File`);
|
|
|
16251
16268
|
const large = lineCount > maxLines ? " ⚠️ [LARGE]" : "";
|
|
16252
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}`);
|
|
16253
16270
|
if (!dryRun)
|
|
16254
|
-
writeLf(
|
|
16271
|
+
writeLf(join22(outputDir, filename), section.content);
|
|
16255
16272
|
return {
|
|
16256
16273
|
index: i2 + 1,
|
|
16257
16274
|
filename,
|
|
@@ -16275,7 +16292,7 @@ chunker - Splitting File`);
|
|
|
16275
16292
|
...lineEnding === "crlf" ? { lineEnding } : {},
|
|
16276
16293
|
...lineEnding === "mixed" ? { lineBreaks: lineBreakRuns(content) } : {}
|
|
16277
16294
|
};
|
|
16278
|
-
const manifestPath =
|
|
16295
|
+
const manifestPath = join22(outputDir, MANIFEST_NAME);
|
|
16279
16296
|
if (!dryRun) {
|
|
16280
16297
|
writeManifest(manifestPath, manifest);
|
|
16281
16298
|
log(`
|
|
@@ -16304,7 +16321,7 @@ To edit: Modify individual chunk files in the directory`);
|
|
|
16304
16321
|
function merge(manifestFile, options, io) {
|
|
16305
16322
|
const log = lineWriter(io);
|
|
16306
16323
|
const dryRun = options.dryRun ?? false;
|
|
16307
|
-
const absoluteManifest =
|
|
16324
|
+
const absoluteManifest = resolve8(manifestFile);
|
|
16308
16325
|
if (!existsSync8(absoluteManifest)) {
|
|
16309
16326
|
io.stderr(`Error: Manifest not found: ${absoluteManifest}
|
|
16310
16327
|
`);
|
|
@@ -16317,17 +16334,17 @@ function merge(manifestFile, options, io) {
|
|
|
16317
16334
|
return 1;
|
|
16318
16335
|
}
|
|
16319
16336
|
const manifest = readManifest(absoluteManifest);
|
|
16320
|
-
const chunksDir =
|
|
16337
|
+
const chunksDir = dirname6(absoluteManifest);
|
|
16321
16338
|
const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
|
|
16322
16339
|
const selfError = chunkFolderError(chunksDir, {
|
|
16323
16340
|
"source file": sourcePath,
|
|
16324
|
-
...options.output ? { "output file":
|
|
16341
|
+
...options.output ? { "output file": resolve8(options.output) } : {}
|
|
16325
16342
|
});
|
|
16326
16343
|
if (selfError) {
|
|
16327
16344
|
io.stderr(selfError);
|
|
16328
16345
|
return 1;
|
|
16329
16346
|
}
|
|
16330
|
-
const outside = isOutside(
|
|
16347
|
+
const outside = isOutside(dirname6(chunksDir), sourcePath);
|
|
16331
16348
|
if (outside && !options.output && !options.yes) {
|
|
16332
16349
|
io.stderr(outsideError(sourcePath, chunksDir, "Use -o <file> to name the target, or --yes to confirm."));
|
|
16333
16350
|
return 1;
|
|
@@ -16350,7 +16367,7 @@ chunker - Merging Chunks`);
|
|
|
16350
16367
|
for (const chunk of manifest.chunks) {
|
|
16351
16368
|
const content = readChunk(chunksDir, chunk.filename);
|
|
16352
16369
|
if (content === undefined) {
|
|
16353
|
-
io.stderr(`Error: Missing chunk file: ${
|
|
16370
|
+
io.stderr(`Error: Missing chunk file: ${join22(chunksDir, chunk.filename)}
|
|
16354
16371
|
`);
|
|
16355
16372
|
return 1;
|
|
16356
16373
|
}
|
|
@@ -16383,7 +16400,7 @@ Modified chunks: ${modifiedCount} of ${manifest.chunks.length}`);
|
|
|
16383
16400
|
NOTE: the number of lines changed. All line breaks use the most common original one.`);
|
|
16384
16401
|
}
|
|
16385
16402
|
}
|
|
16386
|
-
const outputPath = options.output ?
|
|
16403
|
+
const outputPath = options.output ? resolve8(options.output) : sourcePath;
|
|
16387
16404
|
if (readSource && existsSync8(sourcePath)) {
|
|
16388
16405
|
const currentSourceHash = hash(readFileSync19(sourcePath, "utf8"));
|
|
16389
16406
|
if (currentSourceHash !== manifest.sourceHash) {
|
|
@@ -16424,7 +16441,7 @@ Merged file written: ${outputPath}`);
|
|
|
16424
16441
|
}
|
|
16425
16442
|
function status(manifestFile, options, io) {
|
|
16426
16443
|
const log = lineWriter(io);
|
|
16427
|
-
const absoluteManifest =
|
|
16444
|
+
const absoluteManifest = resolve8(manifestFile);
|
|
16428
16445
|
if (!existsSync8(absoluteManifest)) {
|
|
16429
16446
|
io.stderr(`Error: Manifest not found: ${absoluteManifest}
|
|
16430
16447
|
`);
|
|
@@ -16437,14 +16454,14 @@ function status(manifestFile, options, io) {
|
|
|
16437
16454
|
return 1;
|
|
16438
16455
|
}
|
|
16439
16456
|
const manifest = readManifest(absoluteManifest);
|
|
16440
|
-
const chunksDir =
|
|
16457
|
+
const chunksDir = dirname6(absoluteManifest);
|
|
16441
16458
|
const sourcePath = resolveSource(chunksDir, manifest.sourceFile);
|
|
16442
16459
|
const selfError = chunkFolderError(chunksDir, { "source file": sourcePath });
|
|
16443
16460
|
if (selfError) {
|
|
16444
16461
|
io.stderr(selfError);
|
|
16445
16462
|
return 1;
|
|
16446
16463
|
}
|
|
16447
|
-
if (isOutside(
|
|
16464
|
+
if (isOutside(dirname6(chunksDir), sourcePath) && !options.yes) {
|
|
16448
16465
|
io.stderr(outsideError(sourcePath, chunksDir, "Use --yes to read it."));
|
|
16449
16466
|
return 1;
|
|
16450
16467
|
}
|
|
@@ -16564,9 +16581,668 @@ ${CHUNK_HELP}`);
|
|
|
16564
16581
|
}
|
|
16565
16582
|
}
|
|
16566
16583
|
|
|
16584
|
+
// src/clones/index.ts
|
|
16585
|
+
import { join as join23, resolve as resolve9 } from "node:path";
|
|
16586
|
+
|
|
16587
|
+
// src/clones/detect.ts
|
|
16588
|
+
function fnv1a(text) {
|
|
16589
|
+
let h = 2166136261;
|
|
16590
|
+
for (let i2 = 0;i2 < text.length; i2++) {
|
|
16591
|
+
h ^= text.charCodeAt(i2);
|
|
16592
|
+
h = Math.imul(h, 16777619);
|
|
16593
|
+
}
|
|
16594
|
+
return h >>> 0;
|
|
16595
|
+
}
|
|
16596
|
+
function groupsBy(units, key) {
|
|
16597
|
+
const groups = new Map;
|
|
16598
|
+
units.forEach((unit, id) => {
|
|
16599
|
+
const ids = groups.get(key(unit));
|
|
16600
|
+
if (ids)
|
|
16601
|
+
ids.push(id);
|
|
16602
|
+
else
|
|
16603
|
+
groups.set(key(unit), [id]);
|
|
16604
|
+
});
|
|
16605
|
+
return [...groups.values()].filter((ids) => ids.length > 1);
|
|
16606
|
+
}
|
|
16607
|
+
function shinglesOf(labels, length) {
|
|
16608
|
+
const set = new Set;
|
|
16609
|
+
for (let i2 = 0;i2 + length <= labels.length; i2++) {
|
|
16610
|
+
set.add(fnv1a(labels.slice(i2, i2 + length).join("|")));
|
|
16611
|
+
}
|
|
16612
|
+
return set;
|
|
16613
|
+
}
|
|
16614
|
+
function nested(a, b) {
|
|
16615
|
+
if (a.file !== b.file)
|
|
16616
|
+
return false;
|
|
16617
|
+
const aInB = b.startIndex <= a.startIndex && a.endIndex <= b.endIndex;
|
|
16618
|
+
const bInA = a.startIndex <= b.startIndex && b.endIndex <= a.endIndex;
|
|
16619
|
+
return aInB || bInA;
|
|
16620
|
+
}
|
|
16621
|
+
function overlap(small, big) {
|
|
16622
|
+
let shared = 0;
|
|
16623
|
+
for (const item of small)
|
|
16624
|
+
if (big.has(item))
|
|
16625
|
+
shared++;
|
|
16626
|
+
return shared;
|
|
16627
|
+
}
|
|
16628
|
+
function round3(value) {
|
|
16629
|
+
return Number(value.toFixed(3));
|
|
16630
|
+
}
|
|
16631
|
+
function detect(units, params) {
|
|
16632
|
+
const exactGroups = groupsBy(units, (u) => u.exactHash);
|
|
16633
|
+
const textGroups = groupsBy(units, (u) => u.textHash);
|
|
16634
|
+
const shingles = units.map((u) => shinglesOf(u.labels, params.shingleLength));
|
|
16635
|
+
const index = new Map;
|
|
16636
|
+
shingles.forEach((set, id) => {
|
|
16637
|
+
for (const shingle of set) {
|
|
16638
|
+
const ids = index.get(shingle);
|
|
16639
|
+
if (ids)
|
|
16640
|
+
ids.push(id);
|
|
16641
|
+
else
|
|
16642
|
+
index.set(shingle, [id]);
|
|
16643
|
+
}
|
|
16644
|
+
});
|
|
16645
|
+
const near = [];
|
|
16646
|
+
units.forEach((unit, id) => {
|
|
16647
|
+
const mine = shingles[id];
|
|
16648
|
+
const shared = new Map;
|
|
16649
|
+
for (const shingle of mine) {
|
|
16650
|
+
const ids = index.get(shingle);
|
|
16651
|
+
if (ids.length > params.rareShingleLimit)
|
|
16652
|
+
continue;
|
|
16653
|
+
for (const other of ids)
|
|
16654
|
+
if (other > id)
|
|
16655
|
+
shared.set(other, (shared.get(other) ?? 0) + 1);
|
|
16656
|
+
}
|
|
16657
|
+
for (const [other, count] of shared) {
|
|
16658
|
+
if (count < params.minSharedShingles)
|
|
16659
|
+
continue;
|
|
16660
|
+
const peer = units[other];
|
|
16661
|
+
if (nested(unit, peer))
|
|
16662
|
+
continue;
|
|
16663
|
+
const theirs = shingles[other];
|
|
16664
|
+
const [small, big] = mine.size <= theirs.size ? [mine, theirs] : [theirs, mine];
|
|
16665
|
+
const inter = overlap(small, big);
|
|
16666
|
+
const jaccard = inter / (mine.size + theirs.size - inter);
|
|
16667
|
+
const contain = inter / small.size;
|
|
16668
|
+
const byContainment = contain >= params.containmentThreshold && small.size >= params.containmentMinShingles;
|
|
16669
|
+
if (jaccard < params.nearThreshold && !byContainment)
|
|
16670
|
+
continue;
|
|
16671
|
+
const common = overlap(unit.names, peer.names);
|
|
16672
|
+
const union = unit.names.size + peer.names.size - common;
|
|
16673
|
+
near.push({
|
|
16674
|
+
a: id,
|
|
16675
|
+
b: other,
|
|
16676
|
+
jStruct: round3(jaccard),
|
|
16677
|
+
contain: round3(contain),
|
|
16678
|
+
jNames: round3(common / (union || 1))
|
|
16679
|
+
});
|
|
16680
|
+
}
|
|
16681
|
+
});
|
|
16682
|
+
near.sort((x, y) => y.jStruct - x.jStruct || x.a - y.a || x.b - y.b);
|
|
16683
|
+
return { exactGroups, textGroups, near };
|
|
16684
|
+
}
|
|
16685
|
+
|
|
16686
|
+
// src/clones/report.ts
|
|
16687
|
+
function inventoryRecord(unit, id) {
|
|
16688
|
+
return {
|
|
16689
|
+
id,
|
|
16690
|
+
file: unit.file,
|
|
16691
|
+
name: unit.name,
|
|
16692
|
+
container: unit.container,
|
|
16693
|
+
kind: unit.kind,
|
|
16694
|
+
exported: unit.exported,
|
|
16695
|
+
startLine: unit.startLine,
|
|
16696
|
+
endLine: unit.endLine,
|
|
16697
|
+
loc: unit.loc,
|
|
16698
|
+
params: unit.params,
|
|
16699
|
+
async: unit.async,
|
|
16700
|
+
nodes: unit.nodes,
|
|
16701
|
+
doc: unit.doc,
|
|
16702
|
+
exactHash: unit.exactHash,
|
|
16703
|
+
textHash: unit.textHash
|
|
16704
|
+
};
|
|
16705
|
+
}
|
|
16706
|
+
function inventoryJsonl(units) {
|
|
16707
|
+
return units.map((unit, id) => `${JSON.stringify(inventoryRecord(unit, id))}
|
|
16708
|
+
`).join("");
|
|
16709
|
+
}
|
|
16710
|
+
function listJson(items) {
|
|
16711
|
+
if (items.length === 0)
|
|
16712
|
+
return "[]";
|
|
16713
|
+
return `[
|
|
16714
|
+
${items.map((item) => ` ${JSON.stringify(item)}`).join(`,
|
|
16715
|
+
`)}
|
|
16716
|
+
]`;
|
|
16717
|
+
}
|
|
16718
|
+
function clonesJson(config, detection) {
|
|
16719
|
+
const { out: _out, parseErrorList: _list, ...params } = config;
|
|
16720
|
+
return [
|
|
16721
|
+
"{",
|
|
16722
|
+
` "params": ${JSON.stringify(params)},`,
|
|
16723
|
+
` "exactGroups": ${listJson(detection.exactGroups)},`,
|
|
16724
|
+
` "textGroups": ${listJson(detection.textGroups)},`,
|
|
16725
|
+
` "near": ${listJson(detection.near)}`,
|
|
16726
|
+
"}",
|
|
16727
|
+
""
|
|
16728
|
+
].join(`
|
|
16729
|
+
`);
|
|
16730
|
+
}
|
|
16731
|
+
var SUMMARY_BANDS = [
|
|
16732
|
+
["0.9 or more", 0.9, Number.POSITIVE_INFINITY],
|
|
16733
|
+
["0.8 to 0.9", 0.8, 0.9],
|
|
16734
|
+
["0.7 to 0.8", 0.7, 0.8],
|
|
16735
|
+
["0.6 to 0.7", 0.6, 0.7],
|
|
16736
|
+
["below 0.6 (containment)", 0, 0.6]
|
|
16737
|
+
];
|
|
16738
|
+
function folderOf(file) {
|
|
16739
|
+
const parts2 = file.split("/").slice(0, -1);
|
|
16740
|
+
return parts2.length === 0 ? "." : parts2.slice(0, 2).join("/");
|
|
16741
|
+
}
|
|
16742
|
+
function percentile(sorted, p) {
|
|
16743
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] ?? 0;
|
|
16744
|
+
}
|
|
16745
|
+
function summaryText(config, stats, units, detection) {
|
|
16746
|
+
const sizes = units.map((u) => u.nodes).sort((a, b) => a - b);
|
|
16747
|
+
const inGroups = detection.exactGroups.reduce((sum, group) => sum + group.length, 0);
|
|
16748
|
+
const bands = SUMMARY_BANDS.map(([label, lo, hi]) => `${label}: ${detection.near.filter((p) => p.jStruct >= lo && p.jStruct < hi).length}`);
|
|
16749
|
+
const byFolder = new Map;
|
|
16750
|
+
for (const unit of units) {
|
|
16751
|
+
const key = folderOf(unit.file);
|
|
16752
|
+
byFolder.set(key, (byFolder.get(key) ?? 0) + 1);
|
|
16753
|
+
}
|
|
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
|
+
];
|
|
16761
|
+
const lines = [
|
|
16762
|
+
`files=${stats.files} parseErrors=${failed} units(minNodes=${config.minNodes})=${units.length}`,
|
|
16763
|
+
...errorList,
|
|
16764
|
+
`nodes p50=${percentile(sizes, 0.5)} p90=${percentile(sizes, 0.9)} p99=${percentile(sizes, 0.99)} max=${sizes[sizes.length - 1] ?? 0}`,
|
|
16765
|
+
`exact groups=${detection.exactGroups.length} (units in them=${inGroups}) text-identical groups=${detection.textGroups.length}`,
|
|
16766
|
+
`near pairs=${detection.near.length} by jStruct: ${bands.join(", ")}`,
|
|
16767
|
+
`units by folder: ${folders}`
|
|
16768
|
+
];
|
|
16769
|
+
return `${lines.join(`
|
|
16770
|
+
`)}
|
|
16771
|
+
`;
|
|
16772
|
+
}
|
|
16773
|
+
|
|
16774
|
+
// src/clones/settings.ts
|
|
16775
|
+
var CLONES_DEFAULTS = Object.freeze({
|
|
16776
|
+
src: "auto",
|
|
16777
|
+
out: null,
|
|
16778
|
+
minNodes: 50,
|
|
16779
|
+
shingleLength: 6,
|
|
16780
|
+
nearThreshold: 0.6,
|
|
16781
|
+
containmentThreshold: 0.9,
|
|
16782
|
+
containmentMinShingles: 20,
|
|
16783
|
+
rareShingleLimit: 60,
|
|
16784
|
+
minSharedShingles: 3,
|
|
16785
|
+
parseErrorList: 20
|
|
16786
|
+
});
|
|
16787
|
+
function mergeClonesConfig(cli, file) {
|
|
16788
|
+
const d = CLONES_DEFAULTS;
|
|
16789
|
+
const src = cli.src ?? file.src ?? d.src;
|
|
16790
|
+
return {
|
|
16791
|
+
src: src === "auto" ? src : [...src],
|
|
16792
|
+
out: cli.out !== undefined ? cli.out : file.out !== undefined ? file.out : d.out,
|
|
16793
|
+
minNodes: cli.minNodes ?? file.minNodes ?? d.minNodes,
|
|
16794
|
+
shingleLength: cli.shingleLength ?? file.shingleLength ?? d.shingleLength,
|
|
16795
|
+
nearThreshold: cli.nearThreshold ?? file.nearThreshold ?? d.nearThreshold,
|
|
16796
|
+
containmentThreshold: cli.containmentThreshold ?? file.containmentThreshold ?? d.containmentThreshold,
|
|
16797
|
+
containmentMinShingles: cli.containmentMinShingles ?? file.containmentMinShingles ?? d.containmentMinShingles,
|
|
16798
|
+
rareShingleLimit: cli.rareShingleLimit ?? file.rareShingleLimit ?? d.rareShingleLimit,
|
|
16799
|
+
minSharedShingles: cli.minSharedShingles ?? file.minSharedShingles ?? d.minSharedShingles,
|
|
16800
|
+
parseErrorList: cli.parseErrorList ?? file.parseErrorList ?? d.parseErrorList
|
|
16801
|
+
};
|
|
16802
|
+
}
|
|
16803
|
+
|
|
16804
|
+
// src/clones/units.ts
|
|
16805
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
16806
|
+
|
|
16807
|
+
// src/clones/normalise.ts
|
|
16808
|
+
var IDENTIFIERS = new Set([
|
|
16809
|
+
"identifier",
|
|
16810
|
+
"property_identifier",
|
|
16811
|
+
"shorthand_property_identifier",
|
|
16812
|
+
"shorthand_property_identifier_pattern",
|
|
16813
|
+
"private_property_identifier",
|
|
16814
|
+
"statement_identifier",
|
|
16815
|
+
"undefined"
|
|
16816
|
+
]);
|
|
16817
|
+
var LITERALS = new Set(["string", "number", "regex", "true", "false", "null"]);
|
|
16818
|
+
var TYPE_SYNTAX = new Set([
|
|
16819
|
+
"type_annotation",
|
|
16820
|
+
"type_arguments",
|
|
16821
|
+
"type_parameters",
|
|
16822
|
+
"type_alias_declaration",
|
|
16823
|
+
"interface_declaration",
|
|
16824
|
+
"enum_declaration",
|
|
16825
|
+
"ambient_declaration",
|
|
16826
|
+
"asserts_annotation",
|
|
16827
|
+
"opting_type_annotation",
|
|
16828
|
+
"omitting_type_annotation",
|
|
16829
|
+
"type_predicate_annotation",
|
|
16830
|
+
"accessibility_modifier",
|
|
16831
|
+
"override_modifier",
|
|
16832
|
+
"decorator",
|
|
16833
|
+
"comment"
|
|
16834
|
+
]);
|
|
16835
|
+
var TRANSPARENT = new Set([
|
|
16836
|
+
"parenthesized_expression",
|
|
16837
|
+
"as_expression",
|
|
16838
|
+
"satisfies_expression",
|
|
16839
|
+
"non_null_expression",
|
|
16840
|
+
"type_assertion",
|
|
16841
|
+
"instantiation_expression"
|
|
16842
|
+
]);
|
|
16843
|
+
var TEMPLATE_TEXT = new Set(["string_fragment", "escape_sequence"]);
|
|
16844
|
+
function wrapped(node) {
|
|
16845
|
+
const children = node.namedChildren;
|
|
16846
|
+
return (node.type === "type_assertion" ? children[children.length - 1] : children[0]) ?? null;
|
|
16847
|
+
}
|
|
16848
|
+
function walk2(node, out2) {
|
|
16849
|
+
if (!node.isNamed)
|
|
16850
|
+
return;
|
|
16851
|
+
const type = node.type;
|
|
16852
|
+
if (TYPE_SYNTAX.has(type))
|
|
16853
|
+
return;
|
|
16854
|
+
if (TRANSPARENT.has(type)) {
|
|
16855
|
+
const inner = wrapped(node);
|
|
16856
|
+
if (inner)
|
|
16857
|
+
walk2(inner, out2);
|
|
16858
|
+
return;
|
|
16859
|
+
}
|
|
16860
|
+
if (IDENTIFIERS.has(type)) {
|
|
16861
|
+
out2.labels.push("ID");
|
|
16862
|
+
out2.names.add(node.text.toLowerCase());
|
|
16863
|
+
return;
|
|
16864
|
+
}
|
|
16865
|
+
if (LITERALS.has(type)) {
|
|
16866
|
+
out2.labels.push("LIT");
|
|
16867
|
+
return;
|
|
16868
|
+
}
|
|
16869
|
+
if (TEMPLATE_TEXT.has(type)) {
|
|
16870
|
+
out2.labels.push("TPL");
|
|
16871
|
+
return;
|
|
16872
|
+
}
|
|
16873
|
+
if (type !== "template_substitution") {
|
|
16874
|
+
const operator = node.childForFieldName("operator");
|
|
16875
|
+
out2.labels.push(operator ? `${type}:${operator.type}` : type);
|
|
16876
|
+
}
|
|
16877
|
+
for (const child of node.children)
|
|
16878
|
+
walk2(child, out2);
|
|
16879
|
+
}
|
|
16880
|
+
function normalise(body2) {
|
|
16881
|
+
const out2 = { labels: [], names: new Set };
|
|
16882
|
+
walk2(body2, out2);
|
|
16883
|
+
return out2;
|
|
16884
|
+
}
|
|
16885
|
+
|
|
16886
|
+
// src/clones/units.ts
|
|
16887
|
+
var DOC_LIMIT = 160;
|
|
16888
|
+
var FUNCTION_VALUES = new Set([
|
|
16889
|
+
"arrow_function",
|
|
16890
|
+
"function_expression",
|
|
16891
|
+
"function",
|
|
16892
|
+
"generator_function"
|
|
16893
|
+
]);
|
|
16894
|
+
var CLASSES = new Set(["class_declaration", "abstract_class_declaration", "class"]);
|
|
16895
|
+
var ANONYMOUS_CLASS = "(class)";
|
|
16896
|
+
function sha1(text) {
|
|
16897
|
+
return createHash2("sha1").update(text).digest("hex").slice(0, 16);
|
|
16898
|
+
}
|
|
16899
|
+
function keyName(key) {
|
|
16900
|
+
if (!key)
|
|
16901
|
+
return null;
|
|
16902
|
+
return key.type === "string" ? key.text.slice(1, -1) : key.text;
|
|
16903
|
+
}
|
|
16904
|
+
function hasToken(node, token) {
|
|
16905
|
+
return node.children.some((c) => !c.isNamed && c.type === token);
|
|
16906
|
+
}
|
|
16907
|
+
function anchorOf(node) {
|
|
16908
|
+
let anchor = node;
|
|
16909
|
+
if (node.type === "variable_declarator" && node.parent)
|
|
16910
|
+
anchor = node.parent;
|
|
16911
|
+
if (anchor.parent?.type === "export_statement")
|
|
16912
|
+
anchor = anchor.parent;
|
|
16913
|
+
return anchor;
|
|
16914
|
+
}
|
|
16915
|
+
function declared(node, container) {
|
|
16916
|
+
switch (node.type) {
|
|
16917
|
+
case "function_declaration":
|
|
16918
|
+
case "generator_function_declaration": {
|
|
16919
|
+
const name2 = keyName(node.childForFieldName("name"));
|
|
16920
|
+
if (!name2)
|
|
16921
|
+
return null;
|
|
16922
|
+
return { fn: node, name: name2, kind: container ? "local" : "function", anchor: anchorOf(node) };
|
|
16923
|
+
}
|
|
16924
|
+
case "method_definition": {
|
|
16925
|
+
const name2 = keyName(node.childForFieldName("name"));
|
|
16926
|
+
if (!name2)
|
|
16927
|
+
return null;
|
|
16928
|
+
const inClass = node.parent?.type === "class_body";
|
|
16929
|
+
let kind = "prop";
|
|
16930
|
+
if (inClass) {
|
|
16931
|
+
if (name2 === "constructor")
|
|
16932
|
+
kind = "ctor";
|
|
16933
|
+
else if (hasToken(node, "get"))
|
|
16934
|
+
kind = "get";
|
|
16935
|
+
else if (hasToken(node, "set"))
|
|
16936
|
+
kind = "set";
|
|
16937
|
+
else
|
|
16938
|
+
kind = "method";
|
|
16939
|
+
}
|
|
16940
|
+
return { fn: node, name: name2, kind, anchor: node };
|
|
16941
|
+
}
|
|
16942
|
+
case "public_field_definition": {
|
|
16943
|
+
const value = node.childForFieldName("value");
|
|
16944
|
+
const name2 = keyName(node.childForFieldName("name"));
|
|
16945
|
+
if (!value || !name2 || !FUNCTION_VALUES.has(value.type))
|
|
16946
|
+
return null;
|
|
16947
|
+
return { fn: value, name: name2, kind: "field", anchor: node };
|
|
16948
|
+
}
|
|
16949
|
+
case "variable_declarator": {
|
|
16950
|
+
const value = node.childForFieldName("value");
|
|
16951
|
+
const id = node.childForFieldName("name");
|
|
16952
|
+
if (!value || !id || id.type !== "identifier" || !FUNCTION_VALUES.has(value.type)) {
|
|
16953
|
+
return null;
|
|
16954
|
+
}
|
|
16955
|
+
return {
|
|
16956
|
+
fn: value,
|
|
16957
|
+
name: id.text,
|
|
16958
|
+
kind: container ? "local" : "function",
|
|
16959
|
+
anchor: anchorOf(node)
|
|
16960
|
+
};
|
|
16961
|
+
}
|
|
16962
|
+
case "pair": {
|
|
16963
|
+
const value = node.childForFieldName("value");
|
|
16964
|
+
const name2 = keyName(node.childForFieldName("key"));
|
|
16965
|
+
if (!value || !name2 || !FUNCTION_VALUES.has(value.type))
|
|
16966
|
+
return null;
|
|
16967
|
+
return { fn: value, name: name2, kind: "prop", anchor: node };
|
|
16968
|
+
}
|
|
16969
|
+
default:
|
|
16970
|
+
return null;
|
|
16971
|
+
}
|
|
16972
|
+
}
|
|
16973
|
+
function paramCount(fn) {
|
|
16974
|
+
const list = fn.childForFieldName("parameters");
|
|
16975
|
+
if (list)
|
|
16976
|
+
return list.namedChildren.filter((c) => c.type !== "comment").length;
|
|
16977
|
+
return fn.childForFieldName("parameter") ? 1 : 0;
|
|
16978
|
+
}
|
|
16979
|
+
function docOf(anchor, source) {
|
|
16980
|
+
const comment = anchor.previousSibling;
|
|
16981
|
+
if (comment?.type !== "comment" || !comment.text.startsWith("/**"))
|
|
16982
|
+
return "";
|
|
16983
|
+
if (source.slice(comment.endIndex, anchor.startIndex).trim() !== "")
|
|
16984
|
+
return "";
|
|
16985
|
+
const lines = comment.text.slice(3, -2).split(`
|
|
16986
|
+
`).map((line) => line.replace(/^\s*\*\s?/, "").trim());
|
|
16987
|
+
const tag = lines.findIndex((line) => line.startsWith("@"));
|
|
16988
|
+
const text = (tag === -1 ? lines : lines.slice(0, tag)).filter((line) => line !== "");
|
|
16989
|
+
return text.join(" ").slice(0, DOC_LIMIT);
|
|
16990
|
+
}
|
|
16991
|
+
function buildUnit(d, container, source, file, minNodes) {
|
|
16992
|
+
const body2 = d.fn.childForFieldName("body");
|
|
16993
|
+
if (!body2)
|
|
16994
|
+
return null;
|
|
16995
|
+
const { labels, names } = normalise(body2);
|
|
16996
|
+
if (labels.length < minNodes)
|
|
16997
|
+
return null;
|
|
16998
|
+
const startLine = d.anchor.startPosition.row + 1;
|
|
16999
|
+
const endLine = d.fn.endPosition.row + 1;
|
|
17000
|
+
return {
|
|
17001
|
+
file,
|
|
17002
|
+
name: d.name,
|
|
17003
|
+
container,
|
|
17004
|
+
kind: d.kind,
|
|
17005
|
+
exported: d.anchor.type === "export_statement",
|
|
17006
|
+
startLine,
|
|
17007
|
+
endLine,
|
|
17008
|
+
loc: endLine - startLine + 1,
|
|
17009
|
+
params: paramCount(d.fn),
|
|
17010
|
+
async: hasToken(d.fn, "async"),
|
|
17011
|
+
nodes: labels.length,
|
|
17012
|
+
doc: docOf(d.anchor, source),
|
|
17013
|
+
exactHash: sha1(labels.join(" ")),
|
|
17014
|
+
textHash: sha1(source.slice(body2.startIndex, body2.endIndex).replace(/\s+/g, "")),
|
|
17015
|
+
startIndex: d.fn.startIndex,
|
|
17016
|
+
endIndex: d.fn.endIndex,
|
|
17017
|
+
labels,
|
|
17018
|
+
names
|
|
17019
|
+
};
|
|
17020
|
+
}
|
|
17021
|
+
function unitsOf(root, source, file, minNodes) {
|
|
17022
|
+
const units = [];
|
|
17023
|
+
const visit = (node, container) => {
|
|
17024
|
+
const found = declared(node, container);
|
|
17025
|
+
if (found) {
|
|
17026
|
+
const unit = buildUnit(found, container, source, file, minNodes);
|
|
17027
|
+
if (unit)
|
|
17028
|
+
units.push(unit);
|
|
17029
|
+
}
|
|
17030
|
+
let inner = container;
|
|
17031
|
+
if (CLASSES.has(node.type)) {
|
|
17032
|
+
inner = keyName(node.childForFieldName("name")) ?? (container || ANONYMOUS_CLASS);
|
|
17033
|
+
}
|
|
17034
|
+
for (const child of node.namedChildren)
|
|
17035
|
+
visit(child, inner);
|
|
17036
|
+
};
|
|
17037
|
+
visit(root, "");
|
|
17038
|
+
return units;
|
|
17039
|
+
}
|
|
17040
|
+
|
|
17041
|
+
// src/clones/index.ts
|
|
17042
|
+
var CLONES_HELP = `Usage: repo-tools clones [root] [--out=<dir>] [--config=<path>] [--src=<path>...]
|
|
17043
|
+
[--min-nodes=<n>] [--shingle=<n>] [--near=<f>]
|
|
17044
|
+
[--containment=<f>] [--containment-min=<n>]
|
|
17045
|
+
[--rare=<n>] [--min-shared=<n>] [--list-errors=<n>]
|
|
17046
|
+
|
|
17047
|
+
List the functions of a TypeScript or JavaScript repository and find the functions
|
|
17048
|
+
that copy each other. The command only reads the repository. The result is a list
|
|
17049
|
+
of candidates: read both bodies of a pair before you change anything.
|
|
17050
|
+
|
|
17051
|
+
A function is a unit when it has a name and its body has at least --min-nodes
|
|
17052
|
+
labels. A label is one node of the body, with names, literals, types and comments
|
|
17053
|
+
erased. Units with the same label sequence are an exact group. Two units whose
|
|
17054
|
+
shingles (runs of --shingle labels) overlap enough are a near pair.
|
|
17055
|
+
|
|
17056
|
+
--out=<dir> Write inventory.jsonl and clones.json to this folder.
|
|
17057
|
+
Without it the command writes no file.
|
|
17058
|
+
--config=<path> The config file. The default is repo-tools.config.json at
|
|
17059
|
+
the root. The settings are in its "clones" section.
|
|
17060
|
+
--src=<path>... The folders to read. The word "auto" (the default) reads
|
|
17061
|
+
the files of the src area. The flag takes every argument
|
|
17062
|
+
up to the next flag.
|
|
17063
|
+
--min-nodes=<n> The smallest number of labels of a unit. Default 50.
|
|
17064
|
+
--shingle=<n> The labels in one shingle. Default 6.
|
|
17065
|
+
--near=<f> The smallest Jaccard score of a near pair. Default 0.6.
|
|
17066
|
+
--containment=<f> The smallest containment of a near pair. Default 0.9.
|
|
17067
|
+
--containment-min=<n> The smallest shingle count of the smaller unit for the
|
|
17068
|
+
containment rule. Default 20.
|
|
17069
|
+
--rare=<n> A shingle in more units than this does not pick candidate
|
|
17070
|
+
pairs. Default 60.
|
|
17071
|
+
--min-shared=<n> The rare shingles that a candidate pair must share.
|
|
17072
|
+
Default 3.
|
|
17073
|
+
--list-errors=<n> The most paths of files with a parse error that the
|
|
17074
|
+
summary lists. Default 20.
|
|
17075
|
+
--root=<path> The root. The default is the current folder.
|
|
17076
|
+
--help, -h Show this help.
|
|
17077
|
+
|
|
17078
|
+
A flag overrides the config file, and the config file overrides the default.
|
|
17079
|
+
|
|
17080
|
+
Exit codes: 0 when the run finished. 1 when the root holds no source file to read.
|
|
17081
|
+
2 on a usage error, a bad config file or a root that is not a folder.
|
|
17082
|
+
`;
|
|
17083
|
+
|
|
17084
|
+
class NothingToRead extends Error {
|
|
17085
|
+
}
|
|
17086
|
+
var VALUE_FLAGS2 = {
|
|
17087
|
+
"--out": "out",
|
|
17088
|
+
"--min-nodes": "minNodes",
|
|
17089
|
+
"--shingle": "shingleLength",
|
|
17090
|
+
"--near": "nearThreshold",
|
|
17091
|
+
"--containment": "containmentThreshold",
|
|
17092
|
+
"--containment-min": "containmentMinShingles",
|
|
17093
|
+
"--rare": "rareShingleLimit",
|
|
17094
|
+
"--min-shared": "minSharedShingles",
|
|
17095
|
+
"--list-errors": "parseErrorList"
|
|
17096
|
+
};
|
|
17097
|
+
var NUMERIC = new Set([
|
|
17098
|
+
"minNodes",
|
|
17099
|
+
"shingleLength",
|
|
17100
|
+
"nearThreshold",
|
|
17101
|
+
"containmentThreshold",
|
|
17102
|
+
"containmentMinShingles",
|
|
17103
|
+
"rareShingleLimit",
|
|
17104
|
+
"minSharedShingles",
|
|
17105
|
+
"parseErrorList"
|
|
17106
|
+
]);
|
|
17107
|
+
function parseArgs2(argv) {
|
|
17108
|
+
const options = { root: ".", config: undefined, settings: {} };
|
|
17109
|
+
const seen = new Set;
|
|
17110
|
+
let rootSet = false;
|
|
17111
|
+
const setRoot = (value) => {
|
|
17112
|
+
if (rootSet)
|
|
17113
|
+
throw new Error("the root is set twice (use --root=<path> or one positional path)");
|
|
17114
|
+
rootSet = true;
|
|
17115
|
+
options.root = value;
|
|
17116
|
+
};
|
|
17117
|
+
const once = (flag) => {
|
|
17118
|
+
if (seen.has(flag))
|
|
17119
|
+
throw new Error(`flag ${flag} is set twice`);
|
|
17120
|
+
seen.add(flag);
|
|
17121
|
+
};
|
|
17122
|
+
for (let i2 = 0;i2 < argv.length; i2++) {
|
|
17123
|
+
const arg = argv[i2] ?? "";
|
|
17124
|
+
if (!arg.startsWith("-")) {
|
|
17125
|
+
setRoot(arg);
|
|
17126
|
+
continue;
|
|
17127
|
+
}
|
|
17128
|
+
const eq = arg.indexOf("=");
|
|
17129
|
+
const name2 = eq === -1 ? arg : arg.slice(0, eq);
|
|
17130
|
+
if (name2 === "--src") {
|
|
17131
|
+
once(name2);
|
|
17132
|
+
const paths = [];
|
|
17133
|
+
if (eq !== -1)
|
|
17134
|
+
paths.push(arg.slice(eq + 1));
|
|
17135
|
+
else
|
|
17136
|
+
while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-")) {
|
|
17137
|
+
paths.push(argv[++i2] ?? "");
|
|
17138
|
+
}
|
|
17139
|
+
if (paths.length === 0 || paths.some((p) => p === "")) {
|
|
17140
|
+
throw new Error("flag --src needs a value: --src=<path>");
|
|
17141
|
+
}
|
|
17142
|
+
const only = paths.length === 1 ? paths[0] : undefined;
|
|
17143
|
+
options.settings.src = checkClonesFlag(name2, "src", only === "auto" ? "auto" : paths);
|
|
17144
|
+
continue;
|
|
17145
|
+
}
|
|
17146
|
+
if (name2 !== "--root" && name2 !== "--config" && !Object.hasOwn(VALUE_FLAGS2, name2)) {
|
|
17147
|
+
throw new Error(`unknown flag '${name2}' (see repo-tools clones --help)`);
|
|
17148
|
+
}
|
|
17149
|
+
once(name2);
|
|
17150
|
+
let value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
17151
|
+
if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-")) {
|
|
17152
|
+
value = argv[++i2];
|
|
17153
|
+
}
|
|
17154
|
+
if (value === undefined || value === "") {
|
|
17155
|
+
throw new Error(`flag ${name2} needs a value: ${name2}=<value>`);
|
|
17156
|
+
}
|
|
17157
|
+
if (name2 === "--root")
|
|
17158
|
+
setRoot(value);
|
|
17159
|
+
else if (name2 === "--config")
|
|
17160
|
+
options.config = value;
|
|
17161
|
+
else {
|
|
17162
|
+
const key = VALUE_FLAGS2[name2];
|
|
17163
|
+
const raw = NUMERIC.has(key) ? /^\d+(\.\d+)?$/.test(value) ? Number(value) : NaN : value;
|
|
17164
|
+
Object.assign(options.settings, { [key]: checkClonesFlag(name2, key, raw) });
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
return options;
|
|
17168
|
+
}
|
|
17169
|
+
function folderPrefix(path) {
|
|
17170
|
+
const posix = path.replace(/\\/g, "/").replace(/^(\.\/)+/, "").replace(/\/+$/, "");
|
|
17171
|
+
return posix === "." ? "" : posix;
|
|
17172
|
+
}
|
|
17173
|
+
function selectFiles(root, src) {
|
|
17174
|
+
const found = discover(root, "typescript").filter((f) => !f.path.endsWith(".d.ts"));
|
|
17175
|
+
if (src === "auto")
|
|
17176
|
+
return found.filter((f) => f.area === "src").map((f) => f.path);
|
|
17177
|
+
const folders = src.map(folderPrefix);
|
|
17178
|
+
return found.filter((f) => folders.some((d) => d === "" || f.path === d || f.path.startsWith(`${d}/`))).map((f) => f.path);
|
|
17179
|
+
}
|
|
17180
|
+
async function readUnits(root, files, minNodes) {
|
|
17181
|
+
const units = [];
|
|
17182
|
+
const parseErrorFiles = [];
|
|
17183
|
+
for (const file of files) {
|
|
17184
|
+
const source = readSource(join23(root, file)).replace(/^\uFEFF/, "");
|
|
17185
|
+
const grammar = grammarFor(file);
|
|
17186
|
+
await loadGrammar(grammar);
|
|
17187
|
+
const tree = parserFor(grammar).parse(source);
|
|
17188
|
+
if (!tree) {
|
|
17189
|
+
parseErrorFiles.push(file);
|
|
17190
|
+
continue;
|
|
17191
|
+
}
|
|
17192
|
+
try {
|
|
17193
|
+
if (tree.rootNode.hasError)
|
|
17194
|
+
parseErrorFiles.push(file);
|
|
17195
|
+
units.push(...unitsOf(tree.rootNode, source, file, minNodes));
|
|
17196
|
+
} finally {
|
|
17197
|
+
tree.delete();
|
|
17198
|
+
}
|
|
17199
|
+
}
|
|
17200
|
+
return { units, parseErrorFiles };
|
|
17201
|
+
}
|
|
17202
|
+
async function execute(root, config, io) {
|
|
17203
|
+
const files = selectFiles(root, config.src);
|
|
17204
|
+
if (files.length === 0) {
|
|
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
|
+
}
|
|
17207
|
+
const { units, parseErrorFiles } = await readUnits(root, files, config.minNodes);
|
|
17208
|
+
const detection = detect(units, config);
|
|
17209
|
+
io.stdout(summaryText(config, { files: files.length, parseErrorFiles }, units, detection));
|
|
17210
|
+
if (config.out !== null) {
|
|
17211
|
+
const out2 = resolve9(root, config.out);
|
|
17212
|
+
try {
|
|
17213
|
+
writeLf(join23(out2, "inventory.jsonl"), inventoryJsonl(units));
|
|
17214
|
+
writeLf(join23(out2, "clones.json"), clonesJson(config, detection));
|
|
17215
|
+
} catch {
|
|
17216
|
+
throw new Error(`cannot write the output folder '${config.out}'`);
|
|
17217
|
+
}
|
|
17218
|
+
const shown = config.out.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
17219
|
+
io.stdout(`wrote ${shown}/inventory.jsonl
|
|
17220
|
+
wrote ${shown}/clones.json
|
|
17221
|
+
`);
|
|
17222
|
+
}
|
|
17223
|
+
return 0;
|
|
17224
|
+
}
|
|
17225
|
+
async function run4(argv, io) {
|
|
17226
|
+
let root;
|
|
17227
|
+
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
17228
|
+
try {
|
|
17229
|
+
const options = parseArgs2(argv);
|
|
17230
|
+
root = resolve9(options.root);
|
|
17231
|
+
if (!isDirectory(root))
|
|
17232
|
+
throw new Error("the root <root> is not an existing directory");
|
|
17233
|
+
const file = loadConfigSections(root, options.config).clones;
|
|
17234
|
+
return await execute(root, mergeClonesConfig(options.settings, file), io);
|
|
17235
|
+
} catch (error) {
|
|
17236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17237
|
+
stderr(`repo-tools clones: ${message}
|
|
17238
|
+
`);
|
|
17239
|
+
return error instanceof NothingToRead ? 1 : 2;
|
|
17240
|
+
}
|
|
17241
|
+
}
|
|
17242
|
+
|
|
16567
17243
|
// src/compress/index.ts
|
|
16568
17244
|
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync20, statSync as statSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
16569
|
-
import { basename as basename8, dirname as
|
|
17245
|
+
import { basename as basename8, dirname as dirname7, extname as extname4, join as join24 } from "node:path";
|
|
16570
17246
|
|
|
16571
17247
|
// src/compress/formats.ts
|
|
16572
17248
|
import { extname as extname3 } from "node:path";
|
|
@@ -17199,7 +17875,7 @@ a value, a missing input file and two inputs without --batch are errors. No file
|
|
|
17199
17875
|
var defaultDeps = {
|
|
17200
17876
|
readdir: (dir) => readdirSync3(dir, { withFileTypes: true })
|
|
17201
17877
|
};
|
|
17202
|
-
function
|
|
17878
|
+
function parseArgs3(args2) {
|
|
17203
17879
|
const o = {
|
|
17204
17880
|
input: "",
|
|
17205
17881
|
inputs: [],
|
|
@@ -17322,7 +17998,7 @@ function findFiles(dir, pattern, recursive, deps = defaultDeps) {
|
|
|
17322
17998
|
const scan = (current) => {
|
|
17323
17999
|
const entries = [...deps.readdir(current)].sort((a, b) => compareCodeUnits(a.name, b.name));
|
|
17324
18000
|
for (const entry of entries) {
|
|
17325
|
-
const full =
|
|
18001
|
+
const full = join24(current, entry.name);
|
|
17326
18002
|
if (entry.isDirectory()) {
|
|
17327
18003
|
if (recursive && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
17328
18004
|
scan(full);
|
|
@@ -17339,16 +18015,16 @@ function isDirectory2(path) {
|
|
|
17339
18015
|
}
|
|
17340
18016
|
function compactName(file) {
|
|
17341
18017
|
const ext = extname4(file);
|
|
17342
|
-
return
|
|
18018
|
+
return join24(dirname7(file), `${basename8(file, ext)}.compact${ext}`);
|
|
17343
18019
|
}
|
|
17344
18020
|
function restoredName(file) {
|
|
17345
18021
|
const base = basename8(file);
|
|
17346
18022
|
const stripped = base.replace(/\.compact(?=\.[^.]*$|$)/i, "");
|
|
17347
|
-
const output = stripped === base ? file :
|
|
18023
|
+
const output = stripped === base ? file : join24(dirname7(file), stripped);
|
|
17348
18024
|
if (output !== file)
|
|
17349
18025
|
return output;
|
|
17350
18026
|
const ext = extname4(file);
|
|
17351
|
-
return
|
|
18027
|
+
return join24(dirname7(file), `${basename8(file, ext)}.restored${ext}`);
|
|
17352
18028
|
}
|
|
17353
18029
|
function assertCanRestore(output, o) {
|
|
17354
18030
|
if (!o.dryRun && !o.yes && existsSync9(output)) {
|
|
@@ -17508,10 +18184,10 @@ Level: ${o.level}
|
|
|
17508
18184
|
}
|
|
17509
18185
|
return 0;
|
|
17510
18186
|
}
|
|
17511
|
-
async function
|
|
18187
|
+
async function run5(argv, io, deps = defaultDeps) {
|
|
17512
18188
|
let o;
|
|
17513
18189
|
try {
|
|
17514
|
-
o =
|
|
18190
|
+
o = parseArgs3(argv);
|
|
17515
18191
|
} catch (error) {
|
|
17516
18192
|
if (!(error instanceof UsageError))
|
|
17517
18193
|
throw error;
|
|
@@ -17562,16 +18238,16 @@ async function run4(argv, io, deps = defaultDeps) {
|
|
|
17562
18238
|
|
|
17563
18239
|
// src/docs/index.ts
|
|
17564
18240
|
import { lstatSync as lstatSync5, readFileSync as readFileSync23, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
17565
|
-
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";
|
|
17566
18242
|
|
|
17567
18243
|
// src/docs/analyse.ts
|
|
17568
18244
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
17569
|
-
import { join as
|
|
18245
|
+
import { join as join26 } from "node:path";
|
|
17570
18246
|
|
|
17571
18247
|
// src/docs/discovery.ts
|
|
17572
18248
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
17573
18249
|
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, statSync as statSync9 } from "node:fs";
|
|
17574
|
-
import { join as
|
|
18250
|
+
import { join as join25 } from "node:path";
|
|
17575
18251
|
var PYTHON_SUFFIXES2 = new Set([".py"]);
|
|
17576
18252
|
var TYPESCRIPT_SUFFIXES = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
17577
18253
|
var SKIP_DIRS2 = new Set([
|
|
@@ -17655,8 +18331,8 @@ function walkSource(root) {
|
|
|
17655
18331
|
const rel = `${prefix}${entry.name}`;
|
|
17656
18332
|
if (entry.isDirectory()) {
|
|
17657
18333
|
if (!SKIP_DIRS2.has(entry.name))
|
|
17658
|
-
go(
|
|
17659
|
-
} else if (isSource(rel) && statOk(
|
|
18334
|
+
go(join25(dir, entry.name), `${rel}/`);
|
|
18335
|
+
} else if (isSource(rel) && statOk(join25(dir, entry.name))) {
|
|
17660
18336
|
found.push(rel);
|
|
17661
18337
|
}
|
|
17662
18338
|
}
|
|
@@ -17672,7 +18348,7 @@ function statOk(path) {
|
|
|
17672
18348
|
}
|
|
17673
18349
|
}
|
|
17674
18350
|
function loadExclusions(root) {
|
|
17675
|
-
const file =
|
|
18351
|
+
const file = join25(root, CONFIG_NAME);
|
|
17676
18352
|
if (!existsSync10(file) || !statOk(file))
|
|
17677
18353
|
return [];
|
|
17678
18354
|
let data;
|
|
@@ -18280,13 +18956,13 @@ var DECLARATIONS = {
|
|
|
18280
18956
|
var DECLARATION_TYPES = Object.keys(DECLARATIONS);
|
|
18281
18957
|
var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
|
|
18282
18958
|
var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
|
|
18283
|
-
var
|
|
18959
|
+
var EXPORT_TYPE_STAR2 = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
|
|
18284
18960
|
function sanitise(source) {
|
|
18285
|
-
return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(
|
|
18961
|
+
return source.replace(ABSTRACT_PROPERTY, "abstrac_$1").replace(EXPORT_TYPE_STAR2, "$1 $2");
|
|
18286
18962
|
}
|
|
18287
|
-
var
|
|
18963
|
+
var IDENTIFIERS2 = new Set(["identifier", "shorthand_property_identifier_pattern"]);
|
|
18288
18964
|
function boundIdentifiers(node, text) {
|
|
18289
|
-
if (
|
|
18965
|
+
if (IDENTIFIERS2.has(node.type))
|
|
18290
18966
|
return [text(node)];
|
|
18291
18967
|
const pattern = node.childForFieldName("pattern");
|
|
18292
18968
|
if (pattern)
|
|
@@ -18304,7 +18980,7 @@ function boundIdentifiers(node, text) {
|
|
|
18304
18980
|
case "rest_pattern":
|
|
18305
18981
|
return into(node.namedChildren[0] ?? null);
|
|
18306
18982
|
default: {
|
|
18307
|
-
const first = node.namedChildren.find((c) =>
|
|
18983
|
+
const first = node.namedChildren.find((c) => IDENTIFIERS2.has(c.type));
|
|
18308
18984
|
return first ? [text(first)] : [];
|
|
18309
18985
|
}
|
|
18310
18986
|
}
|
|
@@ -18313,7 +18989,7 @@ function paramsOf2(node, text) {
|
|
|
18313
18989
|
const list = node.childForFieldName("parameters");
|
|
18314
18990
|
return (list?.namedChildren ?? []).flatMap((param) => boundIdentifiers(param, text));
|
|
18315
18991
|
}
|
|
18316
|
-
function
|
|
18992
|
+
function anchorOf2(node) {
|
|
18317
18993
|
let target = node;
|
|
18318
18994
|
while (target.parent?.type === "export_statement")
|
|
18319
18995
|
target = target.parent;
|
|
@@ -18470,7 +19146,7 @@ async function analyseTypeScript(path, source) {
|
|
|
18470
19146
|
const text = (node) => source.slice(node.startIndex, node.endIndex);
|
|
18471
19147
|
for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
|
|
18472
19148
|
const kind = DECLARATIONS[node.type];
|
|
18473
|
-
const anchor =
|
|
19149
|
+
const anchor = anchorOf2(node);
|
|
18474
19150
|
const lead = stubAnchor(anchor, source, text);
|
|
18475
19151
|
const doc = leadingDoc(anchor, node, text);
|
|
18476
19152
|
const dialect = dialectOf(doc);
|
|
@@ -18527,7 +19203,7 @@ async function analyseFile(root, rel) {
|
|
|
18527
19203
|
const language = languageOf(rel);
|
|
18528
19204
|
let source;
|
|
18529
19205
|
try {
|
|
18530
|
-
source = readSourceText2(
|
|
19206
|
+
source = readSourceText2(join26(root, rel));
|
|
18531
19207
|
} catch (error) {
|
|
18532
19208
|
const report = newReport(rel, language);
|
|
18533
19209
|
report.error = `unreadable: ${readFailure(error)}`;
|
|
@@ -18544,7 +19220,7 @@ async function analyseRepo(root) {
|
|
|
18544
19220
|
}
|
|
18545
19221
|
|
|
18546
19222
|
// src/docs/report.ts
|
|
18547
|
-
import { join as
|
|
19223
|
+
import { join as join27 } from "node:path";
|
|
18548
19224
|
function percent3(part, whole) {
|
|
18549
19225
|
if (whole === 0)
|
|
18550
19226
|
return 100;
|
|
@@ -18656,8 +19332,8 @@ function symbolJson(s) {
|
|
|
18656
19332
|
};
|
|
18657
19333
|
}
|
|
18658
19334
|
function writeOutputs(outDir, reports, stats, provenance, rootName) {
|
|
18659
|
-
const markdown =
|
|
18660
|
-
const json =
|
|
19335
|
+
const markdown = join27(outDir, "COVERAGE.md");
|
|
19336
|
+
const json = join27(outDir, "coverage.json");
|
|
18661
19337
|
writeReport(markdown, renderMarkdown(reports, stats, provenance, rootName));
|
|
18662
19338
|
const payload = {
|
|
18663
19339
|
summary: statsJson(stats),
|
|
@@ -18861,7 +19537,7 @@ Exit codes: 0 when the action succeeded and the gate passed. 1 when the gate
|
|
|
18861
19537
|
failed, or when the root holds no source file to measure. 2 on a usage error,
|
|
18862
19538
|
when the root is not a folder, or when ${CONFIG_NAME} is malformed.
|
|
18863
19539
|
`;
|
|
18864
|
-
var
|
|
19540
|
+
var VALUE_FLAGS3 = {
|
|
18865
19541
|
"--root": "all",
|
|
18866
19542
|
"--out": "scan",
|
|
18867
19543
|
"--path": "stub",
|
|
@@ -18914,8 +19590,8 @@ function parseDocsArgs(argv) {
|
|
|
18914
19590
|
while (i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
|
|
18915
19591
|
options.paths.push(argv[++i2] ?? "");
|
|
18916
19592
|
}
|
|
18917
|
-
} else if (Object.hasOwn(
|
|
18918
|
-
forbid(name2,
|
|
19593
|
+
} else if (Object.hasOwn(VALUE_FLAGS3, name2)) {
|
|
19594
|
+
forbid(name2, VALUE_FLAGS3[name2]);
|
|
18919
19595
|
let value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
18920
19596
|
if (eq === -1 && i2 + 1 < argv.length && !(argv[i2 + 1] ?? "").startsWith("-"))
|
|
18921
19597
|
value = argv[++i2];
|
|
@@ -19009,7 +19685,7 @@ async function runScan(root, options, io) {
|
|
|
19009
19685
|
if (reports.length === 0)
|
|
19010
19686
|
throw new NothingToMeasure(provenance);
|
|
19011
19687
|
const stats = summarise(reports);
|
|
19012
|
-
const outDir =
|
|
19688
|
+
const outDir = resolve10(options.out ?? join28(root, "docs", "code-docs"));
|
|
19013
19689
|
const written = writeOutputs(outDir, reports, stats, provenance, basename9(root));
|
|
19014
19690
|
io.stdout(`scanned ${stats.filesScanned} file(s) via ${provenance}
|
|
19015
19691
|
`);
|
|
@@ -19049,7 +19725,7 @@ async function runStub(root, options, io) {
|
|
|
19049
19725
|
const targets = report.symbols.filter((s) => s.issues.some((i2) => i2.rule === "M1"));
|
|
19050
19726
|
if (targets.length === 0)
|
|
19051
19727
|
continue;
|
|
19052
|
-
const file =
|
|
19728
|
+
const file = join28(root, report.path);
|
|
19053
19729
|
if (leavesRoot(root, file)) {
|
|
19054
19730
|
io.stdout(` SKIPPED ${report.path}: the path is a link, or it resolves outside the root
|
|
19055
19731
|
`);
|
|
@@ -19175,7 +19851,7 @@ async function runCheck(root, options, io) {
|
|
|
19175
19851
|
`);
|
|
19176
19852
|
return 1;
|
|
19177
19853
|
}
|
|
19178
|
-
async function
|
|
19854
|
+
async function run6(argv, io) {
|
|
19179
19855
|
if (wantsHelp(argv)) {
|
|
19180
19856
|
io.stdout(DOCS_HELP);
|
|
19181
19857
|
return 0;
|
|
@@ -19184,7 +19860,7 @@ async function run5(argv, io) {
|
|
|
19184
19860
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
19185
19861
|
try {
|
|
19186
19862
|
const options = parseDocsArgs(argv);
|
|
19187
|
-
root =
|
|
19863
|
+
root = resolve10(options.root);
|
|
19188
19864
|
if (!isDirectory(root))
|
|
19189
19865
|
throw new Error("the root <root> is not an existing directory");
|
|
19190
19866
|
assertReadableLanguage(root);
|
|
@@ -19203,14 +19879,14 @@ async function run5(argv, io) {
|
|
|
19203
19879
|
|
|
19204
19880
|
// src/query/index.ts
|
|
19205
19881
|
import { existsSync as existsSync12, statSync as statSync10 } from "node:fs";
|
|
19206
|
-
import { resolve as
|
|
19882
|
+
import { resolve as resolve11 } from "node:path";
|
|
19207
19883
|
|
|
19208
19884
|
// src/query/args.ts
|
|
19209
19885
|
var MODE_FLAGS = {
|
|
19210
19886
|
"--emit": { name: "emit" },
|
|
19211
19887
|
"--check-browser-safety": { name: "check-browser-safety" }
|
|
19212
19888
|
};
|
|
19213
|
-
var
|
|
19889
|
+
var VALUE_FLAGS4 = new Set(["--root", "--config", "--out", "--node-runtime"]);
|
|
19214
19890
|
var COMMAND_USAGE = {
|
|
19215
19891
|
dependents: "<file>",
|
|
19216
19892
|
"symbol-users": "<symbol>",
|
|
@@ -19291,7 +19967,7 @@ function parseQueryArgs(argv, cwd) {
|
|
|
19291
19967
|
mode = modeCommand;
|
|
19292
19968
|
continue;
|
|
19293
19969
|
}
|
|
19294
|
-
if (!
|
|
19970
|
+
if (!VALUE_FLAGS4.has(name2)) {
|
|
19295
19971
|
throw new Error(`unknown flag '${name2}' (see repo-tools query --help)`);
|
|
19296
19972
|
}
|
|
19297
19973
|
if (value === undefined || value === "") {
|
|
@@ -19780,12 +20456,12 @@ report, a path that is not a file of the graph, a browser-safety command on a
|
|
|
19780
20456
|
graph that is not TypeScript/JavaScript, or a browser-safety leak with
|
|
19781
20457
|
--check-browser-safety.
|
|
19782
20458
|
`;
|
|
19783
|
-
async function
|
|
20459
|
+
async function run7(argv, io) {
|
|
19784
20460
|
let root;
|
|
19785
20461
|
const stderr = (text) => io.stderr(root ? maskRoot(text, root) : text);
|
|
19786
20462
|
try {
|
|
19787
20463
|
const options = parseQueryArgs(argv, process.cwd());
|
|
19788
|
-
root =
|
|
20464
|
+
root = resolve11(options.root);
|
|
19789
20465
|
if (!(existsSync12(root) && statSync10(root).isDirectory())) {
|
|
19790
20466
|
throw new Error("the root <root> is not an existing directory");
|
|
19791
20467
|
}
|
|
@@ -19821,7 +20497,7 @@ async function run6(argv, io) {
|
|
|
19821
20497
|
|
|
19822
20498
|
// src/ste/index.ts
|
|
19823
20499
|
import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5, readFileSync as readFileSync26, statSync as statSync11 } from "node:fs";
|
|
19824
|
-
import { basename as
|
|
20500
|
+
import { basename as basename11, join as join30, resolve as resolve13 } from "node:path";
|
|
19825
20501
|
|
|
19826
20502
|
// src/ste/markdown.ts
|
|
19827
20503
|
function stripNoise(text) {
|
|
@@ -19876,7 +20552,12 @@ function wordCount(sentence) {
|
|
|
19876
20552
|
const s = sentence.replace(CODE_SPAN, "X").replace(LINK, "$1").replace(EMPHASIS_CHARS, "").replace(LIST_MARKER, "");
|
|
19877
20553
|
return pySplit(s).filter((w) => Array.from(w).some(isAlnum)).length;
|
|
19878
20554
|
}
|
|
19879
|
-
|
|
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) {
|
|
19880
20561
|
const chunks = [];
|
|
19881
20562
|
const lineMap = [];
|
|
19882
20563
|
parts2.forEach(([n, body2], i2) => {
|
|
@@ -19906,7 +20587,7 @@ function paragraphs(text) {
|
|
|
19906
20587
|
for (const [n, line] of stripNoise(text)) {
|
|
19907
20588
|
const startsItem = LIST_ITEM.test(line);
|
|
19908
20589
|
if ((n !== prevLine + 1 || startsItem) && para.length > 0) {
|
|
19909
|
-
blocks.push(
|
|
20590
|
+
blocks.push(join29(para, paraStart));
|
|
19910
20591
|
para = [];
|
|
19911
20592
|
}
|
|
19912
20593
|
if (para.length === 0)
|
|
@@ -19915,42 +20596,112 @@ function paragraphs(text) {
|
|
|
19915
20596
|
prevLine = n;
|
|
19916
20597
|
}
|
|
19917
20598
|
if (para.length > 0)
|
|
19918
|
-
blocks.push(
|
|
20599
|
+
blocks.push(join29(para, paraStart));
|
|
19919
20600
|
return blocks;
|
|
19920
20601
|
}
|
|
19921
|
-
function
|
|
19922
|
-
const
|
|
20602
|
+
function checkMarkdownFindings(name2, text) {
|
|
20603
|
+
const found = [];
|
|
19923
20604
|
const blocks = paragraphs(text);
|
|
19924
|
-
for (const [start2, block] of blocks) {
|
|
20605
|
+
for (const [start2, block, lineMap] of blocks) {
|
|
19925
20606
|
const procedural = PROCEDURAL_STARTS.some((p) => pyLstrip(block).startsWith(p));
|
|
19926
20607
|
const limit = procedural ? MAX_WORDS_PROCEDURAL : MAX_WORDS_DESCRIPTIVE;
|
|
20608
|
+
const last = lineMap.length > 0 ? lineMap[lineMap.length - 1] : start2;
|
|
19927
20609
|
for (const sentence of sentences(block)) {
|
|
19928
20610
|
const n = wordCount(sentence);
|
|
19929
|
-
if (n > limit) {
|
|
20611
|
+
if (n > limit && !isIdentifierList(sentence)) {
|
|
19930
20612
|
const tier = procedural ? "procedural" : "descriptive";
|
|
19931
|
-
|
|
20613
|
+
found.push({
|
|
20614
|
+
text: `${name2}:~${start2}: ${n} words (>${limit} ${tier}): ${headCodePoints(pyStrip(sentence), 90)}`,
|
|
20615
|
+
first: start2,
|
|
20616
|
+
last
|
|
20617
|
+
});
|
|
19932
20618
|
}
|
|
19933
20619
|
}
|
|
19934
20620
|
}
|
|
19935
20621
|
for (const [, block, lineMap] of blocks) {
|
|
19936
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
|
+
};
|
|
19937
20627
|
for (const [phrase, better, re] of WORDY_RULES) {
|
|
19938
20628
|
for (const m of bare.matchAll(re)) {
|
|
19939
|
-
|
|
20629
|
+
found.push(at(m.index, `wordy: ${pyRepr(phrase)} - prefer ${pyRepr(better)}`));
|
|
19940
20630
|
}
|
|
19941
20631
|
}
|
|
19942
20632
|
for (const m of bare.matchAll(AMBIGUOUS2)) {
|
|
19943
|
-
|
|
20633
|
+
found.push(at(m.index, `ambiguous reference: ${pyRepr(pyStrip(m[0]))}`));
|
|
19944
20634
|
}
|
|
19945
20635
|
for (const m of bare.matchAll(PASSIVE2)) {
|
|
19946
|
-
|
|
20636
|
+
found.push(at(m.index, `passive voice: ${pyRepr(pyStrip(m[0]))}`));
|
|
19947
20637
|
}
|
|
19948
20638
|
}
|
|
19949
|
-
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);
|
|
19950
20701
|
}
|
|
19951
20702
|
|
|
19952
20703
|
// src/ste/index.ts
|
|
19953
|
-
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>]
|
|
19954
20705
|
repo-tools ste --prose <file>
|
|
19955
20706
|
|
|
19956
20707
|
Check prose against the mechanically decidable part of ASD-STE100 Simplified
|
|
@@ -19960,11 +20711,25 @@ cannot judge the approved dictionary.
|
|
|
19960
20711
|
|
|
19961
20712
|
Markdown mode (the default) checks each Markdown file, or each .md file below a
|
|
19962
20713
|
directory, in code-unit order of the path. A descriptive sentence may have 25
|
|
19963
|
-
words; a procedural one (a numbered step or a check box) may have 20.
|
|
19964
|
-
|
|
19965
|
-
|
|
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
|
|
19966
20720
|
"<!-- ste:historical-record -->" is skipped, and the skip is counted.
|
|
19967
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.
|
|
19968
20733
|
--prose <file> Check the file as one docstring. Tag lines (@param), section
|
|
19969
20734
|
headers, doctest lines and code fences are removed first. Each
|
|
19970
20735
|
finding prints as "RULE<TAB>detail". The findings are advice:
|
|
@@ -19973,7 +20738,9 @@ identifiers are not checked. A file whose first five lines hold
|
|
|
19973
20738
|
|
|
19974
20739
|
Exit codes: 0 when no finding (Markdown mode) or always (--prose mode). 1 when a
|
|
19975
20740
|
Markdown finding exists. 2 on a usage error, when a path does not exist, when no
|
|
19976
|
-
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.
|
|
19977
20744
|
`;
|
|
19978
20745
|
var HISTORICAL_MARKER = "<!-- ste:historical-record -->";
|
|
19979
20746
|
function readText(path) {
|
|
@@ -20002,7 +20769,7 @@ function targetsFor(path) {
|
|
|
20002
20769
|
return;
|
|
20003
20770
|
}
|
|
20004
20771
|
for (const entry of entries) {
|
|
20005
|
-
const full =
|
|
20772
|
+
const full = join30(dir, entry.name);
|
|
20006
20773
|
if (entry.isDirectory()) {
|
|
20007
20774
|
if (entry.name !== ".pytest_cache")
|
|
20008
20775
|
walk(full, [...parts2, entry.name]);
|
|
@@ -20015,7 +20782,7 @@ function targetsFor(path) {
|
|
|
20015
20782
|
};
|
|
20016
20783
|
if (existsSync13(path) && lstatSync6(path).isDirectory())
|
|
20017
20784
|
walk(path, []);
|
|
20018
|
-
return found.sort(comparePaths).map((parts2) =>
|
|
20785
|
+
return found.sort(comparePaths).map((parts2) => join30(path, ...parts2));
|
|
20019
20786
|
}
|
|
20020
20787
|
function isHistoricalRecord(path) {
|
|
20021
20788
|
let text;
|
|
@@ -20037,7 +20804,7 @@ function runProse(args2, io) {
|
|
|
20037
20804
|
try {
|
|
20038
20805
|
text = readText(file);
|
|
20039
20806
|
} catch {
|
|
20040
|
-
io.stderr(`repo-tools ste: cannot read ${
|
|
20807
|
+
io.stderr(`repo-tools ste: cannot read ${basename11(file)} as UTF-8 text
|
|
20041
20808
|
`);
|
|
20042
20809
|
return 2;
|
|
20043
20810
|
}
|
|
@@ -20046,10 +20813,41 @@ function runProse(args2, io) {
|
|
|
20046
20813
|
`);
|
|
20047
20814
|
return 0;
|
|
20048
20815
|
}
|
|
20049
|
-
|
|
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
|
+
}
|
|
20840
|
+
async function run8(argv, io) {
|
|
20050
20841
|
if (argv[0] === "--prose")
|
|
20051
20842
|
return runProse(argv.slice(1), io);
|
|
20052
|
-
|
|
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) {
|
|
20053
20851
|
io.stdout(`usage: repo-tools ste <file-or-directory> [...]
|
|
20054
20852
|
`);
|
|
20055
20853
|
return 2;
|
|
@@ -20057,12 +20855,15 @@ async function run7(argv, io) {
|
|
|
20057
20855
|
let total = 0;
|
|
20058
20856
|
let checked = 0;
|
|
20059
20857
|
let skipped = 0;
|
|
20858
|
+
let hidden = 0;
|
|
20060
20859
|
const missing = [];
|
|
20061
|
-
|
|
20860
|
+
const examined = new Set;
|
|
20861
|
+
for (const arg of paths) {
|
|
20062
20862
|
if (!existsSync13(arg))
|
|
20063
20863
|
missing.push(arg);
|
|
20064
20864
|
for (const target of targetsFor(arg)) {
|
|
20065
|
-
const name2 =
|
|
20865
|
+
const name2 = basename11(target);
|
|
20866
|
+
examined.add(pathKey(target));
|
|
20066
20867
|
if (isHistoricalRecord(target)) {
|
|
20067
20868
|
skipped += 1;
|
|
20068
20869
|
io.stdout(`
|
|
@@ -20078,30 +20879,62 @@ async function run7(argv, io) {
|
|
|
20078
20879
|
`);
|
|
20079
20880
|
return 2;
|
|
20080
20881
|
}
|
|
20081
|
-
|
|
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
|
+
}
|
|
20082
20897
|
checked += 1;
|
|
20083
|
-
total +=
|
|
20084
|
-
const status =
|
|
20898
|
+
total += findings.length;
|
|
20899
|
+
const status = findings.length === 0 ? "OK" : `${findings.length} problem(s)`;
|
|
20085
20900
|
io.stdout(`
|
|
20086
20901
|
=== ${name2}: ${status} ===
|
|
20087
20902
|
`);
|
|
20088
|
-
for (const
|
|
20089
|
-
io.stdout(` ${
|
|
20903
|
+
for (const f of findings)
|
|
20904
|
+
io.stdout(` ${f.text}
|
|
20090
20905
|
`);
|
|
20091
20906
|
}
|
|
20092
20907
|
}
|
|
20093
|
-
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("; ")}` : "";
|
|
20094
20915
|
io.stdout(`
|
|
20095
20916
|
TOTAL: ${total} problem(s) in ${checked} file(s)${tail}
|
|
20096
20917
|
`);
|
|
20097
20918
|
for (const path of missing)
|
|
20098
20919
|
io.stderr(`ERROR: path does not exist: ${path}
|
|
20099
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
|
+
}
|
|
20100
20933
|
if (checked === 0) {
|
|
20101
20934
|
io.stderr(`ERROR: no Markdown file was examined. This is a failure, not a clean result -- check the path(s) given.
|
|
20102
20935
|
`);
|
|
20103
20936
|
}
|
|
20104
|
-
if (missing.length > 0 || checked === 0)
|
|
20937
|
+
if (missing.length > 0 || checked === 0 || requiredFailed)
|
|
20105
20938
|
return 2;
|
|
20106
20939
|
return total > 0 ? 1 : 0;
|
|
20107
20940
|
}
|
|
@@ -20128,7 +20961,7 @@ ${MAP_HELP}`,
|
|
|
20128
20961
|
docs: {
|
|
20129
20962
|
summary: "Measure, stub and gate the doc comments of TypeScript, JavaScript and Python source.",
|
|
20130
20963
|
help: DOCS_HELP,
|
|
20131
|
-
run:
|
|
20964
|
+
run: run6
|
|
20132
20965
|
},
|
|
20133
20966
|
chunk: {
|
|
20134
20967
|
summary: "Split a large file into chunks, merge the chunks back, or show changed chunks.",
|
|
@@ -20138,17 +20971,22 @@ ${MAP_HELP}`,
|
|
|
20138
20971
|
compress: {
|
|
20139
20972
|
summary: "Write a compact copy of a file for a model context, or restore it.",
|
|
20140
20973
|
help: HELP,
|
|
20141
|
-
run:
|
|
20974
|
+
run: run5
|
|
20142
20975
|
},
|
|
20143
20976
|
query: {
|
|
20144
20977
|
summary: "Answer structural questions from the depgraph reports, or write derived reports.",
|
|
20145
20978
|
help: QUERY_HELP,
|
|
20146
|
-
run:
|
|
20979
|
+
run: run7
|
|
20147
20980
|
},
|
|
20148
20981
|
ste: {
|
|
20149
20982
|
summary: "Check Markdown or docstring prose against Simplified Technical English.",
|
|
20150
20983
|
help: STE_HELP,
|
|
20151
|
-
run:
|
|
20984
|
+
run: run8
|
|
20985
|
+
},
|
|
20986
|
+
clones: {
|
|
20987
|
+
summary: "List the functions of a repository and find the functions that copy each other.",
|
|
20988
|
+
help: CLONES_HELP,
|
|
20989
|
+
run: run4
|
|
20152
20990
|
}
|
|
20153
20991
|
};
|
|
20154
20992
|
var SUBCOMMANDS = Object.keys(REGISTRY);
|