@cplieger/deadset-ts 8.0.0-dev.1 → 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.
Files changed (276) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/mapper-connection.js +2 -1
  3. package/dist/bin/node-host.js +24 -5
  4. package/dist/src/analysis.js +181 -341
  5. package/dist/src/augmentations.js +106 -13
  6. package/dist/src/automatic-types.js +10 -29
  7. package/dist/src/build-scripts.js +425 -0
  8. package/dist/src/component-layer.js +4 -2
  9. package/dist/src/components.js +2 -3
  10. package/dist/src/config.js +6 -82
  11. package/dist/src/configuration-files.js +28 -176
  12. package/dist/src/configured-declarations.js +15 -20
  13. package/dist/src/conformance-record.js +1 -1
  14. package/dist/src/consumers.js +3 -30
  15. package/dist/src/contextual-uses.js +56 -9
  16. package/dist/src/contract/config.generated.js +558 -0
  17. package/dist/src/contract/contract.generated.js +1244 -0
  18. package/dist/src/contract/report.generated.js +862 -0
  19. package/dist/src/convention-rows.js +73 -3
  20. package/dist/src/conventions.js +36 -66
  21. package/dist/src/dependencies.js +103 -40
  22. package/dist/src/edges.js +38 -122
  23. package/dist/src/element-access.js +4 -6
  24. package/dist/src/entry-point-gaps.js +2 -8
  25. package/dist/src/entry-points.js +11 -27
  26. package/dist/src/enum-group.js +3 -10
  27. package/dist/src/erased-stores.js +4 -6
  28. package/dist/src/exempt-classes.js +1 -63
  29. package/dist/src/exempt.js +21 -40
  30. package/dist/src/failure.js +36 -0
  31. package/dist/src/file-facts.js +2 -25
  32. package/dist/src/findings/completion.js +139 -72
  33. package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
  34. package/dist/src/findings/emitters.js +5 -5
  35. package/dist/src/findings/interfaces.js +4 -5
  36. package/dist/src/findings/intra-function.js +13 -10
  37. package/dist/src/findings/non-code-artifacts.js +7 -8
  38. package/dist/src/findings/reads-and-writes.js +16 -13
  39. package/dist/src/findings/self-check.js +10 -12
  40. package/dist/src/findings/unused-declarations.js +41 -32
  41. package/dist/src/findings/visibility-narrowing.js +19 -16
  42. package/dist/src/findings-pass.js +8 -15
  43. package/dist/src/framework-lifecycle.js +7 -7
  44. package/dist/src/fs/document.js +52 -0
  45. package/dist/src/fs/manifest.js +72 -0
  46. package/dist/src/fs/tree.js +88 -0
  47. package/dist/src/generated-file.js +7 -4
  48. package/dist/src/git-ignore.js +39 -26
  49. package/dist/src/graph.js +5 -3
  50. package/dist/src/html-entries.js +6 -33
  51. package/dist/src/in-operands.js +2 -1
  52. package/dist/src/index.js +1 -1
  53. package/dist/src/injection-container.js +1 -1
  54. package/dist/src/intra-function-parts.js +63 -43
  55. package/dist/src/inventory.js +2 -21
  56. package/dist/src/javascript-file.js +33 -0
  57. package/dist/src/json-document.js +198 -67
  58. package/dist/src/keyof-comparisons.js +4 -6
  59. package/dist/src/kinds.js +3 -366
  60. package/dist/src/load/classify.js +270 -0
  61. package/dist/src/load/diagnostics.js +80 -0
  62. package/dist/src/load/discover.js +277 -0
  63. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  64. package/dist/src/load/expected-module.js +136 -0
  65. package/dist/src/load/load.js +27 -0
  66. package/dist/src/load/module-lookup.js +37 -0
  67. package/dist/src/load/plan.js +313 -0
  68. package/dist/src/load/specifiers.js +115 -0
  69. package/dist/src/load/synthesize.js +140 -0
  70. package/dist/src/load/test-globals.js +207 -0
  71. package/dist/src/load/test-joins.js +82 -0
  72. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
  73. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
  74. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +23 -65
  75. package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
  76. package/dist/src/{workspace.js → load/workspace.js} +62 -124
  77. package/dist/src/manifest.js +49 -54
  78. package/dist/src/matrix.js +76 -4
  79. package/dist/src/mdx-entries.js +3 -26
  80. package/dist/src/outside-tests.js +1 -1
  81. package/dist/src/partial.js +2 -1
  82. package/dist/src/paths.js +26 -15
  83. package/dist/src/position.js +2 -1
  84. package/dist/src/print.js +11 -79
  85. package/dist/src/product.js +13 -0
  86. package/dist/src/query.js +1 -0
  87. package/dist/src/ref.js +1 -0
  88. package/dist/src/references.js +48 -30
  89. package/dist/src/report-schema.js +51 -0
  90. package/dist/src/report.js +57 -41
  91. package/dist/src/reporters/annotations.js +15 -8
  92. package/dist/src/reporters/sarif.js +16 -10
  93. package/dist/src/reporters/template.js +4 -19
  94. package/dist/src/resolve.js +245 -729
  95. package/dist/src/roots.js +273 -87
  96. package/dist/src/run.js +66 -77
  97. package/dist/src/schema.js +320 -343
  98. package/dist/src/scope.js +38 -115
  99. package/dist/src/serialization-contract.js +595 -21
  100. package/dist/src/session.js +34 -29
  101. package/dist/src/source-files.js +1 -1
  102. package/dist/src/suppress-file.js +25 -28
  103. package/dist/src/suppress-inline.js +3 -2
  104. package/dist/src/suppress.js +33 -51
  105. package/dist/src/sweep.js +7 -9
  106. package/dist/src/symbol-keys.js +3 -5
  107. package/dist/src/template-field.js +9 -19
  108. package/dist/src/test-support.js +8 -11
  109. package/dist/src/text/compare.js +26 -0
  110. package/dist/src/text/glob.js +194 -0
  111. package/dist/src/text/named-files.js +54 -0
  112. package/dist/src/text/shell.js +159 -0
  113. package/dist/src/text/yaml.js +393 -0
  114. package/dist/src/type-errors.js +11 -232
  115. package/dist/src/unseen-loads.js +100 -0
  116. package/dist/src/unwrap.js +23 -0
  117. package/dist/src/usage.js +10 -0
  118. package/dist/src/verbs/analyze.js +166 -126
  119. package/dist/src/verbs/describe.js +9 -16
  120. package/dist/src/verbs/explain.js +10 -7
  121. package/dist/src/verbs/print-config.js +2 -1
  122. package/dist/src/verbs/print-projects.js +6 -4
  123. package/dist/src/verbs/print-retained.js +4 -2
  124. package/dist/src/verbs/print-roots.js +4 -2
  125. package/dist/src/verbs/verb.js +1 -5
  126. package/dist/src/verbs/version.js +14 -3
  127. package/dist/src/workflow-steps.js +62 -64
  128. package/jsr.json +1 -1
  129. package/package.json +4 -3
  130. package/src/analysis.ts +245 -502
  131. package/src/augmentations.ts +135 -17
  132. package/src/automatic-types.ts +11 -29
  133. package/src/build-scripts.ts +543 -0
  134. package/src/component-layer.ts +4 -2
  135. package/src/components.ts +2 -4
  136. package/src/config.ts +31 -302
  137. package/src/configuration-files.ts +34 -179
  138. package/src/configured-declarations.ts +18 -23
  139. package/src/conformance-record.ts +1 -1
  140. package/src/consumers.ts +6 -57
  141. package/src/contextual-uses.ts +64 -12
  142. package/src/contract/config.generated.ts +818 -0
  143. package/src/contract/contract.generated.ts +1308 -0
  144. package/src/contract/report.generated.ts +1237 -0
  145. package/src/convention-rows.ts +85 -3
  146. package/src/conventions.ts +46 -84
  147. package/src/dependencies.ts +118 -46
  148. package/src/edges.ts +41 -140
  149. package/src/element-access.ts +4 -7
  150. package/src/entry-point-gaps.ts +3 -9
  151. package/src/entry-points.ts +12 -38
  152. package/src/enum-group.ts +2 -11
  153. package/src/erased-stores.ts +3 -7
  154. package/src/exempt-classes.ts +1 -75
  155. package/src/exempt.ts +36 -50
  156. package/src/failure.ts +63 -0
  157. package/src/file-facts.ts +9 -24
  158. package/src/finding.ts +14 -9
  159. package/src/findings/boundary.ts +2 -0
  160. package/src/findings/completion.ts +181 -84
  161. package/src/findings/dependencies-and-module-machinery.ts +4 -7
  162. package/src/findings/emitter.ts +8 -9
  163. package/src/findings/emitters.ts +7 -7
  164. package/src/findings/interfaces.ts +4 -6
  165. package/src/findings/intra-function.ts +13 -11
  166. package/src/findings/non-code-artifacts.ts +9 -11
  167. package/src/findings/reads-and-writes.ts +20 -16
  168. package/src/findings/self-check.ts +11 -17
  169. package/src/findings/unused-declarations.ts +45 -32
  170. package/src/findings/visibility-narrowing.ts +20 -17
  171. package/src/findings-pass.ts +10 -19
  172. package/src/framework-lifecycle.ts +7 -7
  173. package/src/fs/document.ts +70 -0
  174. package/src/fs/manifest.ts +92 -0
  175. package/src/fs/tree.ts +110 -0
  176. package/src/generated-file.ts +7 -4
  177. package/src/git-ignore.ts +50 -26
  178. package/src/graph.ts +5 -3
  179. package/src/host.ts +12 -5
  180. package/src/html-entries.ts +6 -33
  181. package/src/in-operands.ts +2 -1
  182. package/src/index.ts +1 -1
  183. package/src/injection-container.ts +1 -1
  184. package/src/intra-function-parts.ts +73 -44
  185. package/src/inventory.ts +2 -20
  186. package/src/javascript-file.ts +37 -0
  187. package/src/json-document.ts +241 -76
  188. package/src/keyof-comparisons.ts +3 -7
  189. package/src/kinds.ts +5 -413
  190. package/src/load/classify.ts +397 -0
  191. package/src/load/diagnostics.ts +144 -0
  192. package/src/load/discover.ts +455 -0
  193. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  194. package/src/load/expected-module.ts +188 -0
  195. package/src/load/load.ts +73 -0
  196. package/src/load/module-lookup.ts +50 -0
  197. package/src/load/plan.ts +504 -0
  198. package/src/load/specifiers.ts +168 -0
  199. package/src/load/synthesize.ts +211 -0
  200. package/src/load/test-globals.ts +251 -0
  201. package/src/load/test-joins.ts +98 -0
  202. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
  203. package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
  204. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +33 -76
  205. package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
  206. package/src/{workspace.ts → load/workspace.ts} +90 -134
  207. package/src/manifest.ts +81 -75
  208. package/src/matrix.ts +122 -4
  209. package/src/mdx-entries.ts +3 -26
  210. package/src/outside-tests.ts +1 -1
  211. package/src/partial.ts +3 -2
  212. package/src/paths.ts +27 -15
  213. package/src/position.ts +2 -1
  214. package/src/print.ts +12 -92
  215. package/src/product.ts +18 -0
  216. package/src/query.ts +11 -0
  217. package/src/ref.ts +1 -0
  218. package/src/references.ts +52 -31
  219. package/src/report-schema.ts +68 -0
  220. package/src/report.ts +101 -224
  221. package/src/reporters/annotations.ts +22 -11
  222. package/src/reporters/json.ts +1 -1
  223. package/src/reporters/reporter.ts +2 -4
  224. package/src/reporters/reporters.ts +1 -1
  225. package/src/reporters/sarif.ts +29 -16
  226. package/src/reporters/template.ts +4 -20
  227. package/src/reporters/text.ts +3 -3
  228. package/src/resolve.ts +283 -1114
  229. package/src/roots.ts +339 -104
  230. package/src/run.ts +75 -91
  231. package/src/schema.ts +391 -388
  232. package/src/scope.ts +46 -138
  233. package/src/serialization-contract.ts +763 -22
  234. package/src/session.ts +70 -54
  235. package/src/source-files.ts +1 -1
  236. package/src/suppress-file.ts +24 -33
  237. package/src/suppress-inline.ts +2 -2
  238. package/src/suppress.ts +43 -58
  239. package/src/sweep.ts +23 -9
  240. package/src/symbol-keys.ts +2 -5
  241. package/src/template-field.ts +15 -25
  242. package/src/test-support.ts +17 -13
  243. package/src/text/compare.ts +27 -0
  244. package/src/text/glob.ts +210 -0
  245. package/src/text/named-files.inputs.json +1522 -0
  246. package/src/text/named-files.ts +68 -0
  247. package/src/text/shell.ts +161 -0
  248. package/src/text/yaml.ts +423 -0
  249. package/src/type-errors.ts +18 -288
  250. package/src/unseen-loads.ts +134 -0
  251. package/src/unwrap.ts +34 -0
  252. package/src/usage.ts +10 -0
  253. package/src/verbs/analyze.ts +224 -162
  254. package/src/verbs/describe.ts +14 -15
  255. package/src/verbs/explain.ts +10 -11
  256. package/src/verbs/print-config.ts +2 -1
  257. package/src/verbs/print-projects.ts +6 -4
  258. package/src/verbs/print-retained.ts +4 -2
  259. package/src/verbs/print-roots.ts +4 -2
  260. package/src/verbs/verb.ts +2 -8
  261. package/src/verbs/version.ts +15 -3
  262. package/src/workflow-steps.ts +74 -64
  263. package/dist/src/base-url.js +0 -18
  264. package/dist/src/discover.js +0 -468
  265. package/dist/src/glob.js +0 -65
  266. package/dist/src/pnpm-workspace.js +0 -120
  267. package/dist/src/setup-failure.js +0 -23
  268. package/dist/src/test-joins.js +0 -101
  269. package/dist/src/version.js +0 -7
  270. package/src/base-url.ts +0 -24
  271. package/src/discover.ts +0 -653
  272. package/src/glob.ts +0 -67
  273. package/src/pnpm-workspace.ts +0 -135
  274. package/src/setup-failure.ts +0 -39
  275. package/src/test-joins.ts +0 -120
  276. package/src/version.ts +0 -7
@@ -4,8 +4,9 @@
4
4
  * verdict a report carries beside them.
5
5
  */
6
6
 
7
+ import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.ts";
7
8
  import type { AnalysisInputs } from "./analysis.ts";
8
- import { fails, type Config } from "./config.ts";
9
+ import { fails, type ResolvedConfig } from "./config.ts";
9
10
  import type { EdgeEvaluation } from "./edges.ts";
10
11
  import type { CompletedFinding, Finding } from "./finding.ts";
11
12
  import { decidedFindings, type Withheld } from "./findings/emitters.ts";
@@ -23,7 +24,8 @@ import {
23
24
  type Ledger,
24
25
  } from "./suppress.ts";
25
26
  import type { Provenance, Recorded } from "./suppress-file.ts";
26
- import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.ts";
27
+ import { bytewise } from "./text/compare.ts";
28
+ import { PRODUCT } from "./product.ts";
27
29
 
28
30
  /** The counts a report's totals carry beside its findings. */
29
31
  interface PassTotals {
@@ -49,25 +51,14 @@ export interface PassResult {
49
51
  readonly ledger: Ledger;
50
52
  }
51
53
 
52
- /** The exit code of a report holding at least one pending finding. */
53
- export const EXIT_PENDING = 4;
54
-
55
- /** Two strings ordered bytewise. */
56
- function compare(a: string, b: string): number {
57
- if (a === b) {
58
- return 0;
59
- }
60
- return a < b ? -1 : 1;
61
- }
62
-
63
54
  /** The canonical order: path, line, column, code, then the symbol reference. */
64
55
  function canonical(a: Finding, b: Finding): number {
65
56
  return (
66
- compare(a.position.path, b.position.path) ||
57
+ bytewise(a.position.path, b.position.path) ||
67
58
  a.position.line - b.position.line ||
68
59
  a.position.column - b.position.column ||
69
- compare(a.code, b.code) ||
70
- compare(a.symbol.ref, b.symbol.ref)
60
+ bytewise(a.code, b.code) ||
61
+ bytewise(a.symbol.ref, b.symbol.ref)
71
62
  );
72
63
  }
73
64
 
@@ -118,7 +109,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
118
109
  * suppression is stale or a finding's severity reaches the configured failing severity;
119
110
  * clean otherwise.
120
111
  */
121
- export function verdictOf(result: PassResult, config: Config): number {
112
+ export function verdictOf(result: PassResult, config: ResolvedConfig): number {
122
113
  if (result.totals.pending > 0) {
123
114
  return EXIT_PENDING;
124
115
  }
@@ -126,7 +117,7 @@ export function verdictOf(result: PassResult, config: Config): number {
126
117
  return EXIT_FINDINGS;
127
118
  }
128
119
  const failing = result.findings.some((finding) =>
129
- fails(finding.severity, config.reporters.failOn),
120
+ fails(finding.severity, config.reporters.fail_on),
130
121
  );
131
122
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
132
123
  }
@@ -164,5 +155,5 @@ export function rowKey(row: Recorded): string {
164
155
 
165
156
  /** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
166
157
  export function analyzerProvenance(version: string): Provenance {
167
- return { analyzer: "deadset-ts", version };
158
+ return { analyzer: PRODUCT, version };
168
159
  }
@@ -55,8 +55,8 @@ function resolveContracts<Brand>(
55
55
  contracts: readonly LifecycleContract[],
56
56
  ): readonly ResolvedContract[] {
57
57
  const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
58
- ...contract.components,
59
- ...contract.bases,
58
+ ...(contract.components ?? []),
59
+ ...(contract.bases ?? []),
60
60
  ]);
61
61
  const resolved = resolveEntries(project, held, entries);
62
62
  let next = 0;
@@ -67,8 +67,8 @@ function resolveContracts<Brand>(
67
67
  };
68
68
  return contracts.map((contract) => ({
69
69
  contract,
70
- components: take(contract.components.length),
71
- bases: take(contract.bases.length),
70
+ components: take(contract.components?.length ?? 0),
71
+ bases: take(contract.bases?.length ?? 0),
72
72
  }));
73
73
  }
74
74
 
@@ -243,11 +243,11 @@ function byBases<Brand>(
243
243
  */
244
244
  export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
245
245
  const { project, held, targetRoot } = input;
246
- if (input.ts.lifecycleContracts.length === 0) {
246
+ if (input.ts.lifecycle_contracts.length === 0) {
247
247
  return [];
248
248
  }
249
249
  const chains = aliasChains(project, held);
250
- const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
250
+ const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
251
251
  const components = [
252
252
  ...byComponents(project, held, chains, contracts),
253
253
  ...byBases(project, held, chains, contracts),
@@ -256,7 +256,7 @@ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly
256
256
  return [];
257
257
  }
258
258
  const ids = new Set(components.map((one) => one.id));
259
- const sides: readonly ClassSide[] = input.ts.lifecycleContracts.some((contract) =>
259
+ const sides: readonly ClassSide[] = input.ts.lifecycle_contracts.some((contract) =>
260
260
  contract.members.some((name) => name.endsWith(":static")),
261
261
  )
262
262
  ? ["instance", "static"]
@@ -0,0 +1,70 @@
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
+
8
+ import { CONFIG_MAX_BYTES } from "../contract/config.generated.ts";
9
+ import type { Host } from "../host.ts";
10
+ import { failureExit } from "../failure.ts";
11
+ import { joinPath, relativePath } from "../paths.ts";
12
+ import { message } from "../product.ts";
13
+ import { utf8Bytes } from "../reporters/sha256.ts";
14
+ import { UsageError } from "../usage.ts";
15
+
16
+ function unreadable(label: string, reason: string): Error {
17
+ return failureExit(
18
+ "unreadable-document",
19
+ `${label}: ${reason}: make it a regular file inside the target root that the run can read, or remove it`,
20
+ );
21
+ }
22
+
23
+ /** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
24
+ export function refuseOverBound(label: string, text: string): void {
25
+ if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
26
+ throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
32
+ * there. One whose name resolves outside the target root or to anything but a regular file,
33
+ * or whose read fails, is refused by `refuse` naming it `label`, nothing read from it. A
34
+ * document past the bound is read one byte past it and refused as {@link refuseOverBound} does.
35
+ */
36
+ export function readTargetDocument(
37
+ host: Host,
38
+ targetRoot: string,
39
+ name: string,
40
+ label = name,
41
+ refuse: (label: string, reason: string) => Error = unreadable,
42
+ ): string | undefined {
43
+ const attempt = <T>(read: () => T): T => {
44
+ try {
45
+ return read();
46
+ } catch (error: unknown) {
47
+ throw refuse(label, message(error));
48
+ }
49
+ };
50
+ const path = joinPath(targetRoot, name);
51
+ const kind = attempt(() => host.kindOf(path));
52
+ if (kind === "absent") {
53
+ return undefined;
54
+ }
55
+ const real = attempt(() => host.realPath(path));
56
+ if (
57
+ relativePath(
58
+ attempt(() => host.realPath(targetRoot)),
59
+ real,
60
+ ) === undefined
61
+ ) {
62
+ throw refuse(label, `resolves to ${real}, outside the target root`);
63
+ }
64
+ if (kind !== "file") {
65
+ throw refuse(label, "is not a regular file");
66
+ }
67
+ const text = attempt(() => host.readFile(real, CONFIG_MAX_BYTES + 1));
68
+ refuseOverBound(label, text);
69
+ return text;
70
+ }
@@ -0,0 +1,92 @@
1
+ import type { Host } from "../host.ts";
2
+ import { isRecord } from "../json-document.ts";
3
+ import { joinPath } from "../paths.ts";
4
+ import { message } from "../product.ts";
5
+
6
+ /** The file a package's manifest is read from. */
7
+ export const MANIFEST = "package.json";
8
+
9
+ /** The directory a package manager installs packages into, whose files are no project's own. */
10
+ export const INSTALLED_PACKAGES = "node_modules";
11
+
12
+ /** The manifest members a package declares the packages it depends on under. */
13
+ export const DEPENDENCY_SECTIONS = [
14
+ "dependencies",
15
+ "devDependencies",
16
+ "peerDependencies",
17
+ "optionalDependencies",
18
+ ] as const;
19
+
20
+ /** Whether one path passes through an installed-packages directory. */
21
+ export function inInstalledPackages(path: string): boolean {
22
+ return path.split("/").includes(INSTALLED_PACKAGES);
23
+ }
24
+
25
+ /** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
26
+ export function readDocument(
27
+ host: Host,
28
+ path: string,
29
+ ): { text: string; value: unknown } | undefined {
30
+ let text: string;
31
+ try {
32
+ text = host.readFile(path);
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ try {
37
+ return { text, value: JSON.parse(text) as unknown };
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ /** One JSON file's decoded value, or undefined where it cannot be read or parsed. */
44
+ export function readJSON(host: Host, path: string): unknown {
45
+ return readDocument(host, path)?.value;
46
+ }
47
+
48
+ /**
49
+ * The manifest in one directory, decoded, or undefined where none reads as an object. A
50
+ * manifest this program cannot read declares nothing, the same answer as an absent one:
51
+ * the package manager owns the refusal of a manifest it rejects.
52
+ */
53
+ export function manifestAt(host: Host, dir: string): Record<string, unknown> | undefined {
54
+ const value = readJSON(host, joinPath(dir, MANIFEST));
55
+ return isRecord(value) ? value : undefined;
56
+ }
57
+
58
+ /**
59
+ * What one directory's manifest reads as: none, an object, a file that does not parse as one,
60
+ * or a file that cannot be read, with the read's error. Either of the last two still bounds
61
+ * the package it would declare.
62
+ */
63
+ export type ManifestRead =
64
+ | { readonly kind: "absent" }
65
+ | { readonly kind: "read"; readonly manifest: Record<string, unknown> }
66
+ | { readonly kind: "malformed" }
67
+ | { readonly kind: "unread"; readonly reason: string };
68
+
69
+ /** The manifest in one directory, as {@link ManifestRead} tells its states apart. */
70
+ export function readManifestAt(host: Host, dir: string): ManifestRead {
71
+ if (!holdsManifest(host, dir)) {
72
+ return { kind: "absent" };
73
+ }
74
+ let text: string;
75
+ try {
76
+ text = host.readFile(joinPath(dir, MANIFEST));
77
+ } catch (error: unknown) {
78
+ return { kind: "unread", reason: message(error) };
79
+ }
80
+ let value: unknown;
81
+ try {
82
+ value = JSON.parse(text);
83
+ } catch {
84
+ return { kind: "malformed" };
85
+ }
86
+ return isRecord(value) ? { kind: "read", manifest: value } : { kind: "malformed" };
87
+ }
88
+
89
+ /** Whether one directory holds a manifest file. */
90
+ export function holdsManifest(host: Host, dir: string): boolean {
91
+ return host.kindOf(joinPath(dir, MANIFEST)) === "file";
92
+ }
package/src/fs/tree.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { DirectoryEntry, Host } from "../host.ts";
2
+ import { ancestors, joinPath, resolvePath } from "../paths.ts";
3
+ import { LANGUAGE_ROWS } from "../contract/contract.generated.ts";
4
+ import { LANGUAGE } from "../product.ts";
5
+ import { bytewise } from "../text/compare.ts";
6
+ import { globExpression } from "../text/glob.ts";
7
+
8
+ /** The directories no walk of the tree enters, by the patterns the language's row names. */
9
+ const SKIPPED_DIRECTORIES = LANGUAGE_ROWS[LANGUAGE].skippedDirectories.map(globExpression);
10
+
11
+ function skipped(name: string): boolean {
12
+ return SKIPPED_DIRECTORIES.some((pattern) => pattern.test(name));
13
+ }
14
+
15
+ /** A git repository's working tree root, and the `info/exclude` file git reads for it. */
16
+ interface Repository {
17
+ readonly root: string;
18
+ /** Undefined where a `.git` file names no repository directory. */
19
+ readonly exclude: string | undefined;
20
+ }
21
+
22
+ /** One file's text without its line ending, or undefined where it cannot be read. */
23
+ function lineOf(host: Host, path: string): string | undefined {
24
+ try {
25
+ return host.readFile(path).replace(/\r?\n$/u, "");
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * The repository whose working tree holds `dir`: the nearest directory at or above it with a
33
+ * `.git`. A `.git` file, which a linked worktree or a submodule holds, names the repository's
34
+ * directory on its `gitdir:` line; that directory's `commondir` file, where it has one, names
35
+ * the directory shared with the main worktree, which holds `info/exclude`:
36
+ * https://git-scm.com/docs/gitrepository-layout
37
+ */
38
+ export function repositoryOf(host: Host, dir: string): Repository | undefined {
39
+ for (const root of ancestors(dir)) {
40
+ const dotGit = joinPath(root, ".git");
41
+ const kind = host.kindOf(dotGit);
42
+ if (kind === "directory") {
43
+ return { root, exclude: joinPath(dotGit, "info/exclude") };
44
+ }
45
+ if (kind === "file") {
46
+ const named = /^gitdir: (.+)$/u.exec(lineOf(host, dotGit) ?? "")?.[1];
47
+ const gitDir = named === undefined ? undefined : resolvePath(root, named);
48
+ const common = gitDir === undefined ? undefined : lineOf(host, joinPath(gitDir, "commondir"));
49
+ const shared =
50
+ gitDir === undefined || common === undefined ? gitDir : resolvePath(gitDir, common);
51
+ return { root, exclude: shared === undefined ? undefined : joinPath(shared, "info/exclude") };
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /** The entries of one directory, none where it cannot be read. */
58
+ export function entriesOf(host: Host, dir: string): readonly DirectoryEntry[] {
59
+ try {
60
+ return host.readDirectory(dir);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /** The names of the files directly in `dir` whose name `accepts`, in bytewise order. */
67
+ export function filesIn(
68
+ host: Host,
69
+ dir: string,
70
+ accepts: (name: string) => boolean,
71
+ ): readonly string[] {
72
+ return entriesOf(host, dir)
73
+ .filter((entry) => !entry.directory && accepts(entry.name))
74
+ .map((entry) => entry.name)
75
+ .sort(bytewise);
76
+ }
77
+
78
+ /** The names of the directories directly in `dir` a walk enters, in bytewise order. */
79
+ export function directoriesIn(host: Host, dir: string): readonly string[] {
80
+ return entriesOf(host, dir)
81
+ .filter((entry) => entry.directory && !skipped(entry.name))
82
+ .map((entry) => entry.name)
83
+ .sort(bytewise);
84
+ }
85
+
86
+ /**
87
+ * Every file below `root` whose name `accepts`, by its path relative to `root` in bytewise
88
+ * order, read in no skipped directory.
89
+ */
90
+ export function filesBelow(
91
+ host: Host,
92
+ root: string,
93
+ accepts: (name: string) => boolean,
94
+ ): readonly string[] {
95
+ const found: string[] = [];
96
+ const pending = [""];
97
+ for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
98
+ for (const entry of entriesOf(host, joinPath(root, dir))) {
99
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
100
+ if (!entry.directory) {
101
+ if (accepts(entry.name)) {
102
+ found.push(path);
103
+ }
104
+ } else if (!skipped(entry.name)) {
105
+ pending.push(path);
106
+ }
107
+ }
108
+ }
109
+ return found.sort(bytewise);
110
+ }
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * The generated-file class on TypeScript: every declaration in a file that marks itself
3
- * generated, or below a directory an applied convention row names as generated, is
4
- * retained, because a generator owns the file and a deletion there is undone at the next
3
+ * generated, below a directory an applied convention row names as generated, or minified,
4
+ * is retained, because a generator owns the file and a deletion there is undone at the next
5
5
  * generation.
6
6
  */
7
7
 
8
8
  import type { DetectorInput, Evidence } from "./exempt.ts";
9
+ import { minified } from "./javascript-file.ts";
9
10
  import type { SourceFiles } from "./source-files.ts";
10
11
 
11
12
  /** The generated-code header, matched against each line of a leading comment. */
@@ -75,7 +76,7 @@ export function generatedByComment(text: string): string | undefined {
75
76
  /**
76
77
  * Every own file of one project that is generated, by its path below the target root,
77
78
  * with the detail a retention in it names: its leading comments' mark, else the row
78
- * naming its directory as generated.
79
+ * naming its directory as generated, else its minified text.
79
80
  */
80
81
  export function generatedFilesOf(
81
82
  files: SourceFiles,
@@ -87,7 +88,9 @@ export function generatedFilesOf(
87
88
  const detail =
88
89
  generatedByComment(file.text) ??
89
90
  (row === undefined
90
- ? undefined
91
+ ? minified(file.fileName, file.text)
92
+ ? "generated as a minified JavaScript file"
93
+ : undefined
91
94
  : `generated below a directory the convention row ${row} names`);
92
95
  if (detail !== undefined) {
93
96
  found.set(path, detail);
package/src/git-ignore.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Which files the `.gitignore` files of a tree ignore, read as git reads them:
2
+ * Which files git ignores below a tree's root, read as git reads its ignore files:
3
3
  * https://git-scm.com/docs/gitignore#_pattern_format
4
4
  */
5
5
 
6
- import { globExpression } from "./glob.ts";
7
6
  import type { Host } from "./host.ts";
8
- import { dirnamePath, joinPath, relativePath } from "./paths.ts";
7
+ import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.ts";
8
+ import { message } from "./product.ts";
9
+ import { gitignoreExpression } from "./text/glob.ts";
10
+ import { repositoryOf } from "./fs/tree.ts";
9
11
 
10
- /** One pattern line of a `.gitignore` file. */
12
+ /** One pattern line of an ignore file. */
11
13
  interface Rule {
12
14
  readonly negated: boolean;
13
15
  /** Whether the pattern ends with `/`, so it matches a directory alone. */
@@ -16,7 +18,7 @@ interface Rule {
16
18
  readonly expression: RegExp;
17
19
  }
18
20
 
19
- /** The rules one `.gitignore` text writes, in order. */
21
+ /** The rules one ignore file's text writes, in order. */
20
22
  function rulesOf(text: string): readonly Rule[] {
21
23
  const rules: Rule[] = [];
22
24
  for (const raw of text.split(/\r?\n/u)) {
@@ -25,7 +27,7 @@ function rulesOf(text: string): readonly Rule[] {
25
27
  continue;
26
28
  }
27
29
  const negated = line.startsWith("!");
28
- line = negated || line.startsWith("\\") ? line.slice(1) : line;
30
+ line = negated ? line.slice(1) : line;
29
31
  const directoryOnly = line.endsWith("/");
30
32
  line = line.replace(/\/+$/u, "");
31
33
  if (line === "") {
@@ -38,43 +40,65 @@ function rulesOf(text: string): readonly Rule[] {
38
40
  rules.push({
39
41
  negated,
40
42
  directoryOnly,
41
- expression: globExpression(anchored ? pattern : `**/${pattern}`),
43
+ expression: gitignoreExpression(anchored ? pattern : `**/${pattern}`),
42
44
  });
43
45
  }
44
46
  return rules;
45
47
  }
46
48
 
49
+ /** Told each ignore file that exists and cannot be read, with why. */
50
+ type UnreadIgnoreFile = (path: string, reason: string) => void;
51
+
52
+ function rulesIn(host: Host, path: string, unread: UnreadIgnoreFile): readonly Rule[] {
53
+ try {
54
+ return rulesOf(host.readFile(path));
55
+ } catch (error: unknown) {
56
+ if (host.kindOf(path) !== "absent") {
57
+ unread(path, message(error));
58
+ }
59
+ return [];
60
+ }
61
+ }
62
+
47
63
  /**
48
- * Whether the `.gitignore` files at and below `root` ignore one absolute path. A path
49
- * outside `root` is not ignored, and a file below an ignored directory stays ignored
50
- * whatever a later pattern says, as git cannot re-include it.
64
+ * Whether git ignores one absolute path below `root`: by the repository's `info/exclude`, then
65
+ * the `.gitignore` files from its root down to the path's directory, a later pattern overriding
66
+ * an earlier one, though a file below an ignored directory stays ignored, as git cannot re-include
67
+ * it. A path outside `root` is not ignored. With no repository, the files at and below `root` are
68
+ * read; with one, the directories above `root` give patterns but are not tested. An unreadable
69
+ * ignore file ignores nothing, and `unread` is told it.
51
70
  */
52
- export function gitIgnored(host: Host, root: string): (path: string) => boolean {
71
+ export function gitIgnored(
72
+ host: Host,
73
+ root: string,
74
+ unread: UnreadIgnoreFile = () => undefined,
75
+ ): (path: string) => boolean {
76
+ const found = repositoryOf(host, root);
77
+ const repository = found?.root;
78
+ const above =
79
+ repository === undefined ? [] : [...ancestors(dirnamePath(root), repository)].reverse();
80
+ const excluded = found?.exclude === undefined ? [] : rulesIn(host, found.exclude, unread);
53
81
  const read = new Map<string, readonly Rule[]>();
54
82
  const rulesAt = (dir: string): readonly Rule[] => {
55
83
  let rules = read.get(dir);
56
84
  if (rules === undefined) {
57
- try {
58
- rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
59
- } catch {
60
- rules = [];
61
- }
85
+ rules = rulesIn(host, joinPath(dir, ".gitignore"), unread);
62
86
  read.set(dir, rules);
63
87
  }
64
88
  return rules;
65
89
  };
66
- /** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
67
90
  const decided = (path: string, directory: boolean): boolean => {
68
- const dirs: string[] = [];
69
- for (let at = dirnamePath(path); relativePath(root, at) !== undefined; at = dirnamePath(at)) {
70
- dirs.unshift(at);
71
- }
72
- dirs.unshift(root);
91
+ const sources: [string, readonly Rule[]][] = [
92
+ ...(repository === undefined ? [] : [[repository, excluded] as [string, readonly Rule[]]]),
93
+ ...[...above, ...[...ancestors(dirnamePath(path), root)].reverse()].map(
94
+ (dir): [string, readonly Rule[]] => [dir, rulesAt(dir)],
95
+ ),
96
+ ];
73
97
  let ignored = false;
74
- for (const dir of dirs) {
75
- const below = relativePath(dir, path);
76
- for (const rule of below === undefined ? [] : rulesAt(dir)) {
77
- if ((directory || !rule.directoryOnly) && rule.expression.test(below ?? "")) {
98
+ for (const [dir, rules] of sources) {
99
+ const below = relativePath(dir, path) ?? "";
100
+ for (const rule of rules) {
101
+ if ((directory || !rule.directoryOnly) && rule.expression.test(below)) {
78
102
  ignored = !rule.negated;
79
103
  }
80
104
  }
package/src/graph.ts CHANGED
@@ -41,9 +41,9 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
42
  * tool reads what it loads and what its strings name, a runtime runs a test, setup or
43
43
  * worker file, a page's module script, a command, a script or a workflow step, the manifest
44
- * names what enters the package, an alias global stands for its export, and an entry file,
45
- * a configured root and a pattern assert a caller. A library's published API only supposes
46
- * an unseen consumer, so it is live under reachability alone.
44
+ * names what enters the package, an alias global stands for its export, an outside
45
+ * declaration names a global interface merging into it, and an entry file, a configured root
46
+ * and a pattern assert a caller. A published API supposes a consumer: reachability alone.
47
47
  */
48
48
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
49
49
  "entry-file": true,
@@ -56,8 +56,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
56
56
  pattern: true,
57
57
  "type-error": true,
58
58
  "type-query-alias": true,
59
+ "global-augmentation": true,
59
60
  "configuration-file": true,
60
61
  "configuration-string": true,
62
+ "bundler-entry": true,
61
63
  "test-runner": true,
62
64
  "browser-tests": true,
63
65
  worker: true,
package/src/host.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** What one path names. */
2
- export type PathKind = "file" | "directory" | "absent";
1
+ /** What one path names: `"other"` is anything else there, a named pipe, a device or a socket. */
2
+ export type PathKind = "file" | "directory" | "other" | "absent";
3
3
 
4
4
  /** One entry of a directory. */
5
5
  export interface DirectoryEntry {
@@ -17,11 +17,18 @@ export interface DirectoryEntry {
17
17
  export interface Host {
18
18
  /** The directory a relative path resolves against. */
19
19
  workingDirectory(): string;
20
- /** The text of one file, or a throw naming why it cannot be read. */
21
- readFile(path: string): string;
20
+ /**
21
+ * The text of one file, or a throw naming why it cannot be read. Given `limit`, the
22
+ * text of no more than the file's first `limit` bytes, so a caller bounding a document
23
+ * asks for one byte past its bound and never holds a larger file whole.
24
+ */
25
+ readFile(path: string, limit?: number): string;
22
26
  /** The entries of one directory, or a throw naming why it cannot be read. */
23
27
  readDirectory(path: string): readonly DirectoryEntry[];
24
- /** What one path names, `"absent"` where nothing is there. */
28
+ /**
29
+ * What one path names once every symbolic link along it is followed, `"absent"` where
30
+ * nothing is there, or a throw naming why it cannot be known, as for a link loop.
31
+ */
25
32
  kindOf(path: string): PathKind;
26
33
  /**
27
34
  * The path one path names once every symbolic link along it is followed, or a throw