@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.0-dev.10
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/README.md +1 -1
- package/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/analysis.js +245 -309
- package/dist/src/augmentations.js +124 -18
- package/dist/src/automatic-types.js +170 -0
- package/dist/src/build-scripts.js +425 -0
- package/dist/src/component-layer.js +4 -2
- package/dist/src/components.js +9 -9
- package/dist/src/config.js +6 -81
- package/dist/src/configuration-files.js +78 -188
- package/dist/src/configured-declarations.js +15 -20
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/consumers.js +3 -30
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/contract/config.generated.js +558 -0
- package/dist/src/contract/contract.generated.js +1244 -0
- package/dist/src/contract/report.generated.js +862 -0
- package/dist/src/convention-rows.js +146 -1
- package/dist/src/conventions.js +136 -70
- package/dist/src/decorator.js +21 -18
- package/dist/src/dependencies.js +280 -62
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +4 -6
- package/dist/src/entry-point-gaps.js +9 -18
- package/dist/src/entry-points.js +106 -21
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +1 -63
- package/dist/src/exempt.js +21 -40
- package/dist/src/failure.js +36 -0
- package/dist/src/file-facts.js +2 -25
- package/dist/src/findings/completion.js +225 -63
- package/dist/src/findings/dependencies-and-module-machinery.js +72 -27
- package/dist/src/findings/emitters.js +6 -6
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +53 -19
- package/dist/src/findings/non-code-artifacts.js +71 -39
- package/dist/src/findings/reads-and-writes.js +16 -13
- package/dist/src/findings/self-check.js +10 -12
- package/dist/src/findings/unused-declarations.js +57 -28
- package/dist/src/findings/visibility-narrowing.js +40 -18
- package/dist/src/findings-pass.js +8 -15
- package/dist/src/framework-lifecycle.js +7 -7
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +72 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/generated-file.js +18 -4
- package/dist/src/git-ignore.js +99 -0
- package/dist/src/graph.js +9 -4
- package/dist/src/html-entries.js +6 -33
- package/dist/src/import-bindings.js +97 -0
- package/dist/src/in-operands.js +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/indexed-access.js +130 -28
- package/dist/src/injection-container.js +1 -1
- package/dist/src/interface-satisfaction.js +30 -12
- package/dist/src/intra-function-parts.js +341 -69
- package/dist/src/inventory.js +54 -29
- package/dist/src/javascript-file.js +33 -0
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +4 -6
- package/dist/src/kinds.js +3 -366
- package/dist/src/load/classify.js +270 -0
- package/dist/src/load/diagnostics.js +80 -0
- package/dist/src/load/discover.js +277 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +27 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/plan.js +313 -0
- package/dist/src/load/specifiers.js +115 -0
- package/dist/src/load/synthesize.js +140 -0
- package/dist/src/load/test-globals.js +207 -0
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +88 -61
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +60 -59
- package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
- package/dist/src/{workspace.js → load/workspace.js} +62 -124
- package/dist/src/manifest.js +49 -54
- package/dist/src/matrix.js +94 -13
- package/dist/src/mdx-entries.js +174 -0
- package/dist/src/module-declarations.js +9 -13
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/overrides.js +41 -10
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +26 -15
- package/dist/src/position.js +2 -1
- package/dist/src/print.js +11 -78
- package/dist/src/product.js +13 -0
- package/dist/src/query.js +1 -0
- package/dist/src/ref.js +11 -0
- package/dist/src/references.js +52 -31
- package/dist/src/report-schema.js +51 -0
- package/dist/src/report.js +59 -41
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +16 -10
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +245 -727
- package/dist/src/roots.js +374 -116
- package/dist/src/run.js +66 -77
- package/dist/src/schema.js +320 -342
- package/dist/src/scope.js +38 -115
- package/dist/src/serialization-contract.js +649 -29
- package/dist/src/session.js +41 -29
- package/dist/src/source-files.js +40 -15
- package/dist/src/suppress-file.js +34 -29
- package/dist/src/suppress-inline.js +21 -4
- package/dist/src/suppress.js +40 -56
- package/dist/src/sweep.js +32 -17
- package/dist/src/symbol-keys.js +105 -6
- package/dist/src/template-field.js +9 -19
- package/dist/src/test-support.js +119 -18
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/named-files.js +54 -0
- package/dist/src/text/shell.js +159 -0
- package/dist/src/text/yaml.js +393 -0
- package/dist/src/type-errors.js +27 -221
- package/dist/src/type-parameter-reads.js +172 -0
- package/dist/src/unseen-loads.js +100 -0
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/verbs/analyze.js +181 -125
- package/dist/src/verbs/describe.js +9 -16
- package/dist/src/verbs/explain.js +14 -9
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +6 -4
- package/dist/src/verbs/print-retained.js +4 -2
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +14 -3
- package/dist/src/workflow-steps.js +128 -33
- package/jsr.json +1 -1
- package/package.json +5 -4
- package/src/analysis.ts +333 -457
- package/src/augmentations.ts +152 -23
- package/src/automatic-types.ts +228 -0
- package/src/build-scripts.ts +543 -0
- package/src/component-layer.ts +4 -2
- package/src/components.ts +9 -10
- package/src/config.ts +31 -299
- package/src/configuration-files.ts +100 -191
- package/src/configured-declarations.ts +18 -23
- package/src/conformance-record.ts +2 -2
- package/src/consumers.ts +6 -57
- package/src/contextual-uses.ts +64 -12
- package/src/contract/config.generated.ts +818 -0
- package/src/contract/contract.generated.ts +1308 -0
- package/src/contract/report.generated.ts +1237 -0
- package/src/convention-rows.ts +186 -1
- package/src/conventions.ts +166 -90
- package/src/decorator.ts +28 -18
- package/src/dependencies.ts +356 -78
- package/src/edges.ts +41 -140
- package/src/element-access.ts +4 -7
- package/src/entry-point-gaps.ts +11 -20
- package/src/entry-points.ts +124 -28
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +1 -75
- package/src/exempt.ts +36 -50
- package/src/failure.ts +63 -0
- package/src/file-facts.ts +9 -24
- package/src/finding.ts +24 -9
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +297 -77
- package/src/findings/dependencies-and-module-machinery.ts +90 -31
- package/src/findings/emitter.ts +8 -9
- package/src/findings/emitters.ts +8 -8
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +64 -20
- package/src/findings/non-code-artifacts.ts +88 -50
- package/src/findings/reads-and-writes.ts +20 -16
- package/src/findings/self-check.ts +11 -17
- package/src/findings/unused-declarations.ts +65 -28
- package/src/findings/visibility-narrowing.ts +51 -19
- package/src/findings-pass.ts +10 -19
- package/src/framework-lifecycle.ts +7 -7
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +92 -0
- package/src/fs/tree.ts +110 -0
- package/src/generated-file.ts +23 -4
- package/src/git-ignore.ts +122 -0
- package/src/graph.ts +11 -3
- package/src/host.ts +17 -14
- package/src/html-entries.ts +6 -33
- package/src/import-bindings.ts +128 -0
- package/src/in-operands.ts +2 -1
- package/src/index.ts +1 -1
- package/src/indexed-access.ts +152 -30
- package/src/injection-container.ts +1 -1
- package/src/interface-satisfaction.ts +33 -13
- package/src/intra-function-parts.ts +449 -85
- package/src/inventory.ts +73 -27
- package/src/javascript-file.ts +37 -0
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +3 -7
- package/src/kinds.ts +5 -413
- package/src/load/classify.ts +397 -0
- package/src/load/diagnostics.ts +144 -0
- package/src/load/discover.ts +455 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +188 -0
- package/src/load/load.ts +73 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/plan.ts +504 -0
- package/src/load/specifiers.ts +168 -0
- package/src/load/synthesize.ts +211 -0
- package/src/load/test-globals.ts +251 -0
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +127 -80
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +73 -64
- package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
- package/src/{workspace.ts → load/workspace.ts} +90 -134
- package/src/manifest.ts +81 -75
- package/src/matrix.ts +154 -18
- package/src/mdx-entries.ts +218 -0
- package/src/module-declarations.ts +11 -13
- package/src/outside-tests.ts +1 -1
- package/src/overrides.ts +51 -12
- package/src/partial.ts +3 -2
- package/src/paths.ts +27 -15
- package/src/position.ts +2 -1
- package/src/print.ts +12 -91
- package/src/product.ts +18 -0
- package/src/query.ts +11 -0
- package/src/ref.ts +13 -0
- package/src/references.ts +62 -32
- package/src/report-schema.ts +68 -0
- package/src/report.ts +105 -215
- package/src/reporters/annotations.ts +22 -11
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -4
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +29 -16
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +283 -1111
- package/src/roots.ts +490 -137
- package/src/run.ts +75 -91
- package/src/schema.ts +391 -387
- package/src/scope.ts +46 -138
- package/src/serialization-contract.ts +818 -26
- package/src/session.ts +77 -54
- package/src/source-files.ts +67 -17
- package/src/suppress-file.ts +36 -34
- package/src/suppress-inline.ts +20 -3
- package/src/suppress.ts +52 -63
- package/src/sweep.ts +53 -18
- package/src/symbol-keys.ts +131 -7
- package/src/template-field.ts +15 -25
- package/src/test-support.ts +155 -18
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/named-files.inputs.json +1522 -0
- package/src/text/named-files.ts +68 -0
- package/src/text/shell.ts +161 -0
- package/src/text/yaml.ts +423 -0
- package/src/type-errors.ts +35 -273
- package/src/type-parameter-reads.ts +227 -0
- package/src/unseen-loads.ts +134 -0
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/verbs/analyze.ts +245 -161
- package/src/verbs/describe.ts +14 -15
- package/src/verbs/explain.ts +15 -13
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +6 -4
- package/src/verbs/print-retained.ts +4 -2
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +2 -8
- package/src/verbs/version.ts +15 -3
- package/src/workflow-steps.ts +149 -34
- package/dist/src/discover.js +0 -347
- package/dist/src/glob.js +0 -65
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/setup-failure.js +0 -23
- package/dist/src/test-joins.js +0 -101
- package/dist/src/version.js +0 -7
- package/src/discover.ts +0 -510
- package/src/glob.ts +0 -67
- package/src/pnpm-workspace.ts +0 -135
- package/src/setup-failure.ts +0 -39
- package/src/test-joins.ts +0 -120
- package/src/version.ts +0 -7
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CODE } from "../contract/contract.generated.js";
|
|
2
|
+
import { testsUnseenAPI } from "./completion.js";
|
|
1
3
|
import { unreachableExports } from "./visibility-narrowing.js";
|
|
2
4
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
3
5
|
const MEMBERS = new Set([
|
|
@@ -7,17 +9,17 @@ const MEMBERS = new Set([
|
|
|
7
9
|
"enum-member",
|
|
8
10
|
]);
|
|
9
11
|
/**
|
|
10
|
-
* The kinds of declaration no code of this
|
|
12
|
+
* The kinds of declaration no code of this emitter reports: an interface and a method an
|
|
11
13
|
* interface declares are the interface kinds' subjects, and a type parameter is the
|
|
12
14
|
* subject of the unused-type-parameter kind or of none.
|
|
13
15
|
*/
|
|
14
|
-
const
|
|
16
|
+
const NOT_REPORTED_HERE = new Set([
|
|
15
17
|
"file",
|
|
16
18
|
"interface",
|
|
17
19
|
"interface-method",
|
|
18
20
|
"type-parameter",
|
|
19
21
|
]);
|
|
20
|
-
/** The reader's word for each kind of declaration a finding of
|
|
22
|
+
/** The reader's word for each kind of declaration a finding of this emitter names. */
|
|
21
23
|
const WORDS = {
|
|
22
24
|
file: "file",
|
|
23
25
|
function: "function",
|
|
@@ -41,15 +43,16 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
41
43
|
* The code one candidate is reported under, or none, each symbol once under the most
|
|
42
44
|
* specific code: a test of dead code first; no other test-file declaration or enum
|
|
43
45
|
* member nothing names; a test-support declaration test code references as test-only
|
|
44
|
-
* use; then deprecation, a test reference,
|
|
45
|
-
* unused export being the
|
|
46
|
+
* use; then deprecation, a test reference, or only dead production references beside a
|
|
47
|
+
* test that stays, and the unreferenced codes, an unimportable unused export being the
|
|
48
|
+
* unreachable kind's.
|
|
46
49
|
*/
|
|
47
50
|
function codeOf(judged) {
|
|
48
51
|
const { candidate, symbol } = judged;
|
|
49
52
|
if (candidate.testOfDeadCode) {
|
|
50
|
-
return
|
|
53
|
+
return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
|
|
51
54
|
}
|
|
52
|
-
if ((judged.test && !candidate.unreferencedTest) ||
|
|
55
|
+
if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
|
|
53
56
|
return undefined;
|
|
54
57
|
}
|
|
55
58
|
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
@@ -60,28 +63,30 @@ function codeOf(judged) {
|
|
|
60
63
|
return unreferencedCode(symbol);
|
|
61
64
|
}
|
|
62
65
|
if (candidate.heldByTests === candidate.id) {
|
|
63
|
-
return
|
|
66
|
+
return CODE.testOnlyUse;
|
|
64
67
|
}
|
|
65
68
|
if (candidate.productionRefs === 0 && judged.deprecated) {
|
|
66
|
-
return
|
|
69
|
+
return CODE.deprecatedUnused;
|
|
67
70
|
}
|
|
68
|
-
if (candidate.
|
|
69
|
-
|
|
71
|
+
if (candidate.testRefs > 0 &&
|
|
72
|
+
(candidate.productionRefs === 0 ||
|
|
73
|
+
(candidate.relation === "reachability" && candidate.keptByTests))) {
|
|
74
|
+
return CODE.testOnlyUse;
|
|
70
75
|
}
|
|
71
76
|
if (MEMBERS.has(symbol.kind)) {
|
|
72
|
-
return
|
|
77
|
+
return CODE.unusedMember;
|
|
73
78
|
}
|
|
74
79
|
if (!symbol.exported) {
|
|
75
|
-
return
|
|
80
|
+
return CODE.unusedUnexported;
|
|
76
81
|
}
|
|
77
|
-
return judged.unreachable ? undefined :
|
|
82
|
+
return judged.unreachable ? undefined : CODE.unusedExported;
|
|
78
83
|
}
|
|
79
84
|
/** The code a declaration nothing references carries, by its shape and its visibility. */
|
|
80
85
|
function unreferencedCode(symbol) {
|
|
81
86
|
if (MEMBERS.has(symbol.kind)) {
|
|
82
|
-
return
|
|
87
|
+
return CODE.unusedMember;
|
|
83
88
|
}
|
|
84
|
-
return symbol.exported ?
|
|
89
|
+
return symbol.exported ? CODE.unusedExported : CODE.unusedUnexported;
|
|
85
90
|
}
|
|
86
91
|
/** The visibility a member's message names, where it narrows the member's reach. */
|
|
87
92
|
function visibilityWord(symbol) {
|
|
@@ -104,14 +109,16 @@ function messageOf(code, judged) {
|
|
|
104
109
|
const { candidate, symbol } = judged;
|
|
105
110
|
const word = WORDS[symbol.kind];
|
|
106
111
|
switch (code) {
|
|
107
|
-
case
|
|
112
|
+
case CODE.testOfDeadCode:
|
|
108
113
|
return TEST_OF_DEAD_CODE;
|
|
109
|
-
case
|
|
114
|
+
case CODE.deprecatedUnused:
|
|
110
115
|
return `deprecated ${word} has no production reference`;
|
|
111
|
-
case
|
|
112
|
-
return
|
|
116
|
+
case CODE.testOnlyUse:
|
|
117
|
+
return candidate.productionRefs === 0
|
|
118
|
+
? `${word} is referenced only from test files and never from production code`
|
|
119
|
+
: `${word} is referenced only from test files and from declarations that are themselves dead`;
|
|
113
120
|
}
|
|
114
|
-
const subject = code ===
|
|
121
|
+
const subject = code === CODE.unusedMember
|
|
115
122
|
? `${visibilityWord(symbol)}${word}`
|
|
116
123
|
: symbol.kind === "export-alias"
|
|
117
124
|
? word
|
|
@@ -119,7 +126,7 @@ function messageOf(code, judged) {
|
|
|
119
126
|
if (candidate.relation === "reachability") {
|
|
120
127
|
return `${subject} is referenced only from declarations that are themselves dead`;
|
|
121
128
|
}
|
|
122
|
-
if (code ===
|
|
129
|
+
if (code === CODE.unusedExported) {
|
|
123
130
|
return `${subject} has no reference in the target and none from any loaded consumer`;
|
|
124
131
|
}
|
|
125
132
|
return `${subject} has no reference in the target`;
|
|
@@ -161,19 +168,40 @@ function brandMembers(input) {
|
|
|
161
168
|
}
|
|
162
169
|
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
163
170
|
const BRANDS = new Set(["type-member", "class-member"]);
|
|
164
|
-
/** The findings of the unused-
|
|
165
|
-
export const unusedDeclarations = (input) =>
|
|
171
|
+
/** The findings of the unused-declaration kinds, `DS1001` to `DS1004` and `DS1102` to `DS1104`. */
|
|
172
|
+
export const unusedDeclarations = (input) => [
|
|
173
|
+
...emitterFindings(input, false),
|
|
174
|
+
...unusedImports(input),
|
|
175
|
+
];
|
|
176
|
+
/**
|
|
177
|
+
* The import bindings their file never reads. A generated file's are the generator's to
|
|
178
|
+
* remove unless the run includes generated files.
|
|
179
|
+
*/
|
|
180
|
+
function unusedImports(input) {
|
|
181
|
+
const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
|
|
182
|
+
return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
|
|
183
|
+
? []
|
|
184
|
+
: [
|
|
185
|
+
{
|
|
186
|
+
code: CODE.unusedImport,
|
|
187
|
+
position: binding.position,
|
|
188
|
+
symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
|
|
189
|
+
message: `import binding ${binding.name} is never read in its file`,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
166
193
|
/**
|
|
167
194
|
* The finding each member of a dead container would carry were its container live: what
|
|
168
195
|
* a record on such a member is matched against, since the report carries the container's
|
|
169
196
|
* finding rather than one of the member's own.
|
|
170
197
|
*/
|
|
171
|
-
export const fallenMembers = (input) =>
|
|
172
|
-
/**
|
|
173
|
-
function
|
|
198
|
+
export const fallenMembers = (input) => emitterFindings(input, true);
|
|
199
|
+
/** This emitter's findings, or, where `fallen` is set, its dead containers' members' alone. */
|
|
200
|
+
function emitterFindings(input, fallen) {
|
|
174
201
|
const union = input.swept.matrix.union;
|
|
175
202
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
176
203
|
const unreachable = unreachableExports(input);
|
|
204
|
+
const unseenAPI = testsUnseenAPI(input);
|
|
177
205
|
let brands;
|
|
178
206
|
const found = [];
|
|
179
207
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -192,10 +220,11 @@ function familyFindings(input, fallen) {
|
|
|
192
220
|
test: union.test[at] === true,
|
|
193
221
|
deprecated: input.deprecated.has(symbol.id),
|
|
194
222
|
unreachable: unreachable.has(symbol.id),
|
|
223
|
+
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
195
224
|
};
|
|
196
225
|
const code = codeOf(judged);
|
|
197
226
|
if (code === undefined ||
|
|
198
|
-
(code ===
|
|
227
|
+
(code === CODE.unusedMember && (brands ??= brandMembers(input)).has(symbol.id))) {
|
|
199
228
|
continue;
|
|
200
229
|
}
|
|
201
230
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
import { CODE } from "../contract/contract.generated.js";
|
|
1
2
|
import { OUTSIDE } from "../graph.js";
|
|
2
3
|
import { declarationKey } from "../ref.js";
|
|
4
|
+
import { runnerCalls } from "../sweep.js";
|
|
3
5
|
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
4
|
-
const UNNECESSARY_EXPORT = "DS1101";
|
|
5
|
-
const UNREACHABLE_EXPORT = "DS1103";
|
|
6
|
-
const REDUNDANT_EXPORT_KEYWORD = "DS1104";
|
|
7
6
|
/**
|
|
8
7
|
* The root kinds by which a consumer importing the package reaches a declaration. A
|
|
9
8
|
* declaration or a file rooted by any other kind is entered by something that names it
|
|
10
9
|
* from outside the reference graph: a runtime, a tool, a framework or the configuration.
|
|
11
10
|
*/
|
|
12
11
|
const PUBLISHING = new Set(["manifest-entry", "published-api"]);
|
|
13
|
-
/** The reader's word for each kind of declaration this
|
|
12
|
+
/** The reader's word for each kind of declaration this emitter reports. */
|
|
14
13
|
const WORDS = {
|
|
15
14
|
function: "function",
|
|
16
15
|
class: "class",
|
|
@@ -24,8 +23,27 @@ const WORDS = {
|
|
|
24
23
|
function publishes(kinds) {
|
|
25
24
|
return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
|
|
26
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The root kinds that name a file without entering it by its exports: a configuration
|
|
28
|
+
* string or a script token runs the file, and no tool reads a name it exports.
|
|
29
|
+
*/
|
|
30
|
+
const RUN_ONLY = new Set(["configuration-string", "script"]);
|
|
31
|
+
/** Whether a root makes its file an entry file, or its declaration an entry, which a tool may read by name. */
|
|
27
32
|
function entersByName(kinds) {
|
|
28
|
-
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
|
|
33
|
+
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The root kinds whose root of a file enters no export by name: a document's import roots
|
|
37
|
+
* each export it imports, so an export it does not import is judged as in any file.
|
|
38
|
+
*/
|
|
39
|
+
const PER_NAME = new Set(["document-import"]);
|
|
40
|
+
/**
|
|
41
|
+
* Whether the roots of a declaration's file enter it by name: a test runner's root
|
|
42
|
+
* enters only an export the runner calls, so another export of a test file is judged.
|
|
43
|
+
*/
|
|
44
|
+
function fileEntersByName(kinds, symbol) {
|
|
45
|
+
const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
|
|
46
|
+
return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
|
|
29
47
|
}
|
|
30
48
|
/**
|
|
31
49
|
* Folds the run's references into the reach of each declaration.
|
|
@@ -89,8 +107,8 @@ function wordOf(symbol) {
|
|
|
89
107
|
}
|
|
90
108
|
/** The visibility each narrowing code names, which only the narrowing codes carry. */
|
|
91
109
|
const NARROWER = {
|
|
92
|
-
[
|
|
93
|
-
[
|
|
110
|
+
[CODE.redundantExportKeyword]: "file",
|
|
111
|
+
[CODE.unnecessaryExport]: "package",
|
|
94
112
|
};
|
|
95
113
|
function findingOf(symbol, code, message) {
|
|
96
114
|
const { path, line, column } = symbol.position;
|
|
@@ -123,7 +141,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
123
141
|
world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
|
|
124
142
|
symbol.kind === "export-alias" ||
|
|
125
143
|
entersByName(facts.rootedBy[at]) ||
|
|
126
|
-
|
|
144
|
+
fileEntersByName(facts.rootedBy[file], symbol) ||
|
|
127
145
|
(facts.named[at] ?? 0) === 0) {
|
|
128
146
|
return undefined;
|
|
129
147
|
}
|
|
@@ -133,12 +151,12 @@ function narrowing(facts, at, symbol, world) {
|
|
|
133
151
|
if (facts.published[file] === true && !closed) {
|
|
134
152
|
return undefined;
|
|
135
153
|
}
|
|
136
|
-
return findingOf(symbol,
|
|
154
|
+
return findingOf(symbol, CODE.redundantExportKeyword, `exported ${word} is referenced only inside the file that declares it`);
|
|
137
155
|
case "package":
|
|
138
156
|
if (!closed || !publishes(facts.rootedBy[at])) {
|
|
139
157
|
return undefined;
|
|
140
158
|
}
|
|
141
|
-
return findingOf(symbol,
|
|
159
|
+
return findingOf(symbol, CODE.unnecessaryExport, `exported ${word} is referenced only inside the package that declares it`);
|
|
142
160
|
default:
|
|
143
161
|
return undefined;
|
|
144
162
|
}
|
|
@@ -148,7 +166,7 @@ function narrowing(facts, at, symbol, world) {
|
|
|
148
166
|
* export no code outside the target can import, because the manifest declares
|
|
149
167
|
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
150
168
|
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
151
|
-
* declaration only a test file references, and a deprecated one.
|
|
169
|
+
* declaration only a test file references or a test that stays keeps, and a deprecated one.
|
|
152
170
|
*/
|
|
153
171
|
function unreachable(facts, at, symbol, candidate, input) {
|
|
154
172
|
if (!input.boundary.encapsulated ||
|
|
@@ -156,17 +174,17 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
156
174
|
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
157
175
|
candidate.testOfDeadCode ||
|
|
158
176
|
symbol.kind === "interface" ||
|
|
159
|
-
(candidate.
|
|
177
|
+
(candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
|
|
160
178
|
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
|
|
161
179
|
return undefined;
|
|
162
180
|
}
|
|
163
181
|
const found = candidate.relation === "reachability"
|
|
164
182
|
? "is referenced only from declarations that are themselves dead"
|
|
165
183
|
: "has no reference in the target";
|
|
166
|
-
return findingOf(symbol,
|
|
184
|
+
return findingOf(symbol, CODE.unreachableExport, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
|
|
167
185
|
}
|
|
168
186
|
/**
|
|
169
|
-
* The visibility-narrowing
|
|
187
|
+
* The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
|
|
170
188
|
* about a symbol a declared cross-language edge names is reported here like any other, and
|
|
171
189
|
* the edge's evaluation then holds it pending, because the edge stands for a reference from
|
|
172
190
|
* outside the file and the package whose use is decided in the other language. A
|
|
@@ -175,17 +193,21 @@ function unreachable(facts, at, symbol, candidate, input) {
|
|
|
175
193
|
export function narrowings(input) {
|
|
176
194
|
const facts = factsOf(input.swept.matrix.union);
|
|
177
195
|
const { declared, loaded } = input.boundary.consumers;
|
|
178
|
-
const closed = input.config.
|
|
196
|
+
const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
|
|
179
197
|
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
180
198
|
const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
181
|
-
const generated = input.config.analysis.
|
|
199
|
+
const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
|
|
182
200
|
const findings = [];
|
|
183
201
|
facts.graph.symbols.forEach((symbol, at) => {
|
|
184
202
|
if (generated?.has(symbol.position.path) === true) {
|
|
185
203
|
return;
|
|
186
204
|
}
|
|
187
205
|
const candidate = candidates.get(symbol.id);
|
|
188
|
-
|
|
206
|
+
// A declaration of test code, a test file's or test-support code's, that only test code
|
|
207
|
+
// references is a candidate, live in the run that counts test references.
|
|
208
|
+
const live = candidate === undefined ||
|
|
209
|
+
(facts.graph.test[at] === true && !candidate.unreferencedTest && !candidate.testOfDeadCode);
|
|
210
|
+
const found = candidate === undefined || live
|
|
189
211
|
? narrowing(facts, at, symbol, world)
|
|
190
212
|
: unreachable(facts, at, symbol, candidate, input);
|
|
191
213
|
if (found !== undefined) {
|
|
@@ -210,5 +232,5 @@ export function unreachableExports(input) {
|
|
|
210
232
|
}
|
|
211
233
|
return found;
|
|
212
234
|
}
|
|
213
|
-
/** The findings of the visibility-narrowing
|
|
235
|
+
/** The findings of the visibility-narrowing kinds, `DS1501`, `DS1101` and `DS1503`. */
|
|
214
236
|
export const visibilityNarrowing = narrowings;
|
|
@@ -3,27 +3,20 @@
|
|
|
3
3
|
* applied and the declared edges evaluated, and the stale suppressions, the totals and the
|
|
4
4
|
* verdict a report carries beside them.
|
|
5
5
|
*/
|
|
6
|
+
import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.js";
|
|
6
7
|
import { fails } from "./config.js";
|
|
7
8
|
import { decidedFindings } from "./findings/emitters.js";
|
|
8
9
|
import { fileRefsOf, staleSuppressions, } from "./findings/self-check.js";
|
|
9
10
|
import { isDependencySubject, isRowSubject, pairs, suppressionCounts, } from "./suppress.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
export const EXIT_PENDING = 4;
|
|
13
|
-
/** Two strings ordered bytewise. */
|
|
14
|
-
function compare(a, b) {
|
|
15
|
-
if (a === b) {
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
18
|
-
return a < b ? -1 : 1;
|
|
19
|
-
}
|
|
11
|
+
import { bytewise } from "./text/compare.js";
|
|
12
|
+
import { PRODUCT } from "./product.js";
|
|
20
13
|
/** The canonical order: path, line, column, code, then the symbol reference. */
|
|
21
14
|
function canonical(a, b) {
|
|
22
|
-
return (
|
|
15
|
+
return (bytewise(a.position.path, b.position.path) ||
|
|
23
16
|
a.position.line - b.position.line ||
|
|
24
17
|
a.position.column - b.position.column ||
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
bytewise(a.code, b.code) ||
|
|
19
|
+
bytewise(a.symbol.ref, b.symbol.ref));
|
|
27
20
|
}
|
|
28
21
|
/**
|
|
29
22
|
* The pass over one run. The emitter table decides every record against every finding the
|
|
@@ -73,7 +66,7 @@ export function verdictOf(result, config) {
|
|
|
73
66
|
if (result.totals.staleSuppressions > 0) {
|
|
74
67
|
return EXIT_FINDINGS;
|
|
75
68
|
}
|
|
76
|
-
const failing = result.findings.some((finding) => fails(finding.severity, config.reporters.
|
|
69
|
+
const failing = result.findings.some((finding) => fails(finding.severity, config.reporters.fail_on));
|
|
77
70
|
return failing ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
78
71
|
}
|
|
79
72
|
/**
|
|
@@ -100,5 +93,5 @@ export function rowKey(row) {
|
|
|
100
93
|
}
|
|
101
94
|
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
|
102
95
|
export function analyzerProvenance(version) {
|
|
103
|
-
return { analyzer:
|
|
96
|
+
return { analyzer: PRODUCT, version };
|
|
104
97
|
}
|
|
@@ -16,8 +16,8 @@ import { must } from "./query.js";
|
|
|
16
16
|
/** Every contract's components and bases, resolved in one pass over the entries. */
|
|
17
17
|
function resolveContracts(project, held, contracts) {
|
|
18
18
|
const entries = contracts.flatMap((contract) => [
|
|
19
|
-
...contract.components,
|
|
20
|
-
...contract.bases,
|
|
19
|
+
...(contract.components ?? []),
|
|
20
|
+
...(contract.bases ?? []),
|
|
21
21
|
]);
|
|
22
22
|
const resolved = resolveEntries(project, held, entries);
|
|
23
23
|
let next = 0;
|
|
@@ -28,8 +28,8 @@ function resolveContracts(project, held, contracts) {
|
|
|
28
28
|
};
|
|
29
29
|
return contracts.map((contract) => ({
|
|
30
30
|
contract,
|
|
31
|
-
components: take(contract.components
|
|
32
|
-
bases: take(contract.bases
|
|
31
|
+
components: take(contract.components?.length ?? 0),
|
|
32
|
+
bases: take(contract.bases?.length ?? 0),
|
|
33
33
|
}));
|
|
34
34
|
}
|
|
35
35
|
/** The classes a decorator or a call the contracts' components name makes a component. */
|
|
@@ -160,11 +160,11 @@ function byBases(project, held, chains, contracts) {
|
|
|
160
160
|
*/
|
|
161
161
|
export function frameworkLifecycle(input) {
|
|
162
162
|
const { project, held, targetRoot } = input;
|
|
163
|
-
if (input.ts.
|
|
163
|
+
if (input.ts.lifecycle_contracts.length === 0) {
|
|
164
164
|
return [];
|
|
165
165
|
}
|
|
166
166
|
const chains = aliasChains(project, held);
|
|
167
|
-
const contracts = resolveContracts(project, held, input.ts.
|
|
167
|
+
const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
|
|
168
168
|
const components = [
|
|
169
169
|
...byComponents(project, held, chains, contracts),
|
|
170
170
|
...byBases(project, held, chains, contracts),
|
|
@@ -173,7 +173,7 @@ export function frameworkLifecycle(input) {
|
|
|
173
173
|
return [];
|
|
174
174
|
}
|
|
175
175
|
const ids = new Set(components.map((one) => one.id));
|
|
176
|
-
const sides = input.ts.
|
|
176
|
+
const sides = input.ts.lifecycle_contracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
|
|
177
177
|
? ["instance", "static"]
|
|
178
178
|
: ["instance"];
|
|
179
179
|
const members = sides.map((side) => classMembers(project, held, ids, side));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read of a document the target holds at its root, which a checkout under analysis
|
|
3
|
+
* controls: the repository configuration, the ignore file, the baseline and the edges
|
|
4
|
+
* document. The read stays inside the target root, takes a regular file alone, and holds no
|
|
5
|
+
* more of it than the Contract's document bound.
|
|
6
|
+
*/
|
|
7
|
+
import { CONFIG_MAX_BYTES } from "../contract/config.generated.js";
|
|
8
|
+
import { failureExit } from "../failure.js";
|
|
9
|
+
import { joinPath, relativePath } from "../paths.js";
|
|
10
|
+
import { message } from "../product.js";
|
|
11
|
+
import { utf8Bytes } from "../reporters/sha256.js";
|
|
12
|
+
import { UsageError } from "../usage.js";
|
|
13
|
+
function unreadable(label, reason) {
|
|
14
|
+
return failureExit("unreadable-document", `${label}: ${reason}: make it a regular file inside the target root that the run can read, or remove it`);
|
|
15
|
+
}
|
|
16
|
+
/** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
|
|
17
|
+
export function refuseOverBound(label, text) {
|
|
18
|
+
if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
|
|
19
|
+
throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
|
|
24
|
+
* there. One whose name resolves outside the target root or to anything but a regular file,
|
|
25
|
+
* or whose read fails, is refused by `refuse` naming it `label`, nothing read from it. A
|
|
26
|
+
* document past the bound is read one byte past it and refused as {@link refuseOverBound} does.
|
|
27
|
+
*/
|
|
28
|
+
export function readTargetDocument(host, targetRoot, name, label = name, refuse = unreadable) {
|
|
29
|
+
const attempt = (read) => {
|
|
30
|
+
try {
|
|
31
|
+
return read();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw refuse(label, message(error));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const path = joinPath(targetRoot, name);
|
|
38
|
+
const kind = attempt(() => host.kindOf(path));
|
|
39
|
+
if (kind === "absent") {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const real = attempt(() => host.realPath(path));
|
|
43
|
+
if (relativePath(attempt(() => host.realPath(targetRoot)), real) === undefined) {
|
|
44
|
+
throw refuse(label, `resolves to ${real}, outside the target root`);
|
|
45
|
+
}
|
|
46
|
+
if (kind !== "file") {
|
|
47
|
+
throw refuse(label, "is not a regular file");
|
|
48
|
+
}
|
|
49
|
+
const text = attempt(() => host.readFile(real, CONFIG_MAX_BYTES + 1));
|
|
50
|
+
refuseOverBound(label, text);
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isRecord } from "../json-document.js";
|
|
2
|
+
import { joinPath } from "../paths.js";
|
|
3
|
+
import { message } from "../product.js";
|
|
4
|
+
/** The file a package's manifest is read from. */
|
|
5
|
+
export const MANIFEST = "package.json";
|
|
6
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
7
|
+
export const INSTALLED_PACKAGES = "node_modules";
|
|
8
|
+
/** The manifest members a package declares the packages it depends on under. */
|
|
9
|
+
export const DEPENDENCY_SECTIONS = [
|
|
10
|
+
"dependencies",
|
|
11
|
+
"devDependencies",
|
|
12
|
+
"peerDependencies",
|
|
13
|
+
"optionalDependencies",
|
|
14
|
+
];
|
|
15
|
+
/** Whether one path passes through an installed-packages directory. */
|
|
16
|
+
export function inInstalledPackages(path) {
|
|
17
|
+
return path.split("/").includes(INSTALLED_PACKAGES);
|
|
18
|
+
}
|
|
19
|
+
/** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
|
|
20
|
+
export function readDocument(host, path) {
|
|
21
|
+
let text;
|
|
22
|
+
try {
|
|
23
|
+
text = host.readFile(path);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return { text, value: JSON.parse(text) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** One JSON file's decoded value, or undefined where it cannot be read or parsed. */
|
|
36
|
+
export function readJSON(host, path) {
|
|
37
|
+
return readDocument(host, path)?.value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The manifest in one directory, decoded, or undefined where none reads as an object. A
|
|
41
|
+
* manifest this program cannot read declares nothing, the same answer as an absent one:
|
|
42
|
+
* the package manager owns the refusal of a manifest it rejects.
|
|
43
|
+
*/
|
|
44
|
+
export function manifestAt(host, dir) {
|
|
45
|
+
const value = readJSON(host, joinPath(dir, MANIFEST));
|
|
46
|
+
return isRecord(value) ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The manifest in one directory, as {@link ManifestRead} tells its states apart. */
|
|
49
|
+
export function readManifestAt(host, dir) {
|
|
50
|
+
if (!holdsManifest(host, dir)) {
|
|
51
|
+
return { kind: "absent" };
|
|
52
|
+
}
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
text = host.readFile(joinPath(dir, MANIFEST));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return { kind: "unread", reason: message(error) };
|
|
59
|
+
}
|
|
60
|
+
let value;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { kind: "malformed" };
|
|
66
|
+
}
|
|
67
|
+
return isRecord(value) ? { kind: "read", manifest: value } : { kind: "malformed" };
|
|
68
|
+
}
|
|
69
|
+
/** Whether one directory holds a manifest file. */
|
|
70
|
+
export function holdsManifest(host, dir) {
|
|
71
|
+
return host.kindOf(joinPath(dir, MANIFEST)) === "file";
|
|
72
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ancestors, joinPath, resolvePath } from "../paths.js";
|
|
2
|
+
import { LANGUAGE_ROWS } from "../contract/contract.generated.js";
|
|
3
|
+
import { LANGUAGE } from "../product.js";
|
|
4
|
+
import { bytewise } from "../text/compare.js";
|
|
5
|
+
import { globExpression } from "../text/glob.js";
|
|
6
|
+
/** The directories no walk of the tree enters, by the patterns the language's row names. */
|
|
7
|
+
const SKIPPED_DIRECTORIES = LANGUAGE_ROWS[LANGUAGE].skippedDirectories.map(globExpression);
|
|
8
|
+
function skipped(name) {
|
|
9
|
+
return SKIPPED_DIRECTORIES.some((pattern) => pattern.test(name));
|
|
10
|
+
}
|
|
11
|
+
/** One file's text without its line ending, or undefined where it cannot be read. */
|
|
12
|
+
function lineOf(host, path) {
|
|
13
|
+
try {
|
|
14
|
+
return host.readFile(path).replace(/\r?\n$/u, "");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The repository whose working tree holds `dir`: the nearest directory at or above it with a
|
|
22
|
+
* `.git`. A `.git` file, which a linked worktree or a submodule holds, names the repository's
|
|
23
|
+
* directory on its `gitdir:` line; that directory's `commondir` file, where it has one, names
|
|
24
|
+
* the directory shared with the main worktree, which holds `info/exclude`:
|
|
25
|
+
* https://git-scm.com/docs/gitrepository-layout
|
|
26
|
+
*/
|
|
27
|
+
export function repositoryOf(host, dir) {
|
|
28
|
+
for (const root of ancestors(dir)) {
|
|
29
|
+
const dotGit = joinPath(root, ".git");
|
|
30
|
+
const kind = host.kindOf(dotGit);
|
|
31
|
+
if (kind === "directory") {
|
|
32
|
+
return { root, exclude: joinPath(dotGit, "info/exclude") };
|
|
33
|
+
}
|
|
34
|
+
if (kind === "file") {
|
|
35
|
+
const named = /^gitdir: (.+)$/u.exec(lineOf(host, dotGit) ?? "")?.[1];
|
|
36
|
+
const gitDir = named === undefined ? undefined : resolvePath(root, named);
|
|
37
|
+
const common = gitDir === undefined ? undefined : lineOf(host, joinPath(gitDir, "commondir"));
|
|
38
|
+
const shared = gitDir === undefined || common === undefined ? gitDir : resolvePath(gitDir, common);
|
|
39
|
+
return { root, exclude: shared === undefined ? undefined : joinPath(shared, "info/exclude") };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** The entries of one directory, none where it cannot be read. */
|
|
45
|
+
export function entriesOf(host, dir) {
|
|
46
|
+
try {
|
|
47
|
+
return host.readDirectory(dir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The names of the files directly in `dir` whose name `accepts`, in bytewise order. */
|
|
54
|
+
export function filesIn(host, dir, accepts) {
|
|
55
|
+
return entriesOf(host, dir)
|
|
56
|
+
.filter((entry) => !entry.directory && accepts(entry.name))
|
|
57
|
+
.map((entry) => entry.name)
|
|
58
|
+
.sort(bytewise);
|
|
59
|
+
}
|
|
60
|
+
/** The names of the directories directly in `dir` a walk enters, in bytewise order. */
|
|
61
|
+
export function directoriesIn(host, dir) {
|
|
62
|
+
return entriesOf(host, dir)
|
|
63
|
+
.filter((entry) => entry.directory && !skipped(entry.name))
|
|
64
|
+
.map((entry) => entry.name)
|
|
65
|
+
.sort(bytewise);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Every file below `root` whose name `accepts`, by its path relative to `root` in bytewise
|
|
69
|
+
* order, read in no skipped directory.
|
|
70
|
+
*/
|
|
71
|
+
export function filesBelow(host, root, accepts) {
|
|
72
|
+
const found = [];
|
|
73
|
+
const pending = [""];
|
|
74
|
+
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
75
|
+
for (const entry of entriesOf(host, joinPath(root, dir))) {
|
|
76
|
+
const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
77
|
+
if (!entry.directory) {
|
|
78
|
+
if (accepts(entry.name)) {
|
|
79
|
+
found.push(path);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (!skipped(entry.name)) {
|
|
83
|
+
pending.push(path);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return found.sort(bytewise);
|
|
88
|
+
}
|