@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.11

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 +5 -4
  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
@@ -1,7 +1,9 @@
1
+ import { CODE } from "../contract/contract.generated.js";
1
2
  import { positionKey } from "../position.js";
2
3
  import { declarationKey } from "../ref.js";
3
- /** The containers whose type parameters `DS1303` reports: a function and a class method. */
4
- const SIGNATURES = new Set(["function", "method"]);
4
+ import { bytewise } from "../text/compare.js";
5
+ /** The containers whose type parameters `DS1006` reports: a function, a class method and a type alias. */
6
+ const SIGNATURES = new Set(["function", "method", "type"]);
5
7
  function indexed(input) {
6
8
  const { matrix, sweep } = input.swept;
7
9
  return {
@@ -75,7 +77,7 @@ function writeOnlyMessage(run, namedBy, symbol, held) {
75
77
  : "never read";
76
78
  const cascade = [
77
79
  ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
78
- ...[...held.alone].sort().map((name) => `import ${name}`),
80
+ ...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
79
81
  ];
80
82
  const word = SUBJECT_WORDS[symbol.kind] ?? "member";
81
83
  const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
@@ -133,7 +135,7 @@ function writeOnlySymbols(run) {
133
135
  run.candidates.has(symbol.id)) {
134
136
  continue;
135
137
  }
136
- found.push(findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
138
+ found.push(findingAbout(symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, namedBy, symbol, held), {
137
139
  writePositions: held.writes,
138
140
  }));
139
141
  }
@@ -157,23 +159,24 @@ function unnamed(run, kind) {
157
159
  });
158
160
  }
159
161
  /**
160
- * `DS1302`: a member of an enum declaration that nothing names. A member an enum-group
162
+ * `DS1005`: a member of an enum declaration that nothing names. A member an enum-group
161
163
  * record holds back is no candidate, so a member a conversion can produce never reaches
162
164
  * this kind.
163
165
  */
164
166
  function unusedEnumMembers(run) {
165
- return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
167
+ return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, CODE.unusedEnumMember, `enum member ${symbol.name} is named nowhere`));
166
168
  }
167
169
  /**
168
- * `DS1303`: a type parameter of a function or a class method that neither the
169
- * declaration's signature nor its body names. A type parameter of a class, an interface,
170
- * a type alias or a method signature belongs to a type declaration and is never
171
- * reported, because a phantom parameter makes two instantiations distinct types.
170
+ * `DS1006`: a type parameter of a function, a class method or a type alias that neither
171
+ * the declaration's signature nor its body names. An alias names no type of its own, so
172
+ * a parameter its type never names makes no two instantiations distinct. One of a class,
173
+ * an interface or a method signature is never reported: a class or an interface merges
174
+ * only with declarations of the same parameters.
172
175
  */
173
176
  function unusedTypeParameters(run) {
174
177
  return unnamed(run, "type-parameter")
175
178
  .filter((symbol) => SIGNATURES.has(run.symbols.get(symbol.parent)?.kind ?? ""))
176
- .map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
179
+ .map((symbol) => findingAbout(symbol, CODE.unusedTypeParameter, `type parameter ${symbol.name} is named in neither its signature nor its body`));
177
180
  }
178
181
  /**
179
182
  * The keys of the declarations the write-only kind reports, whatever the
@@ -184,8 +187,8 @@ export function writeOnlyDeclarations(input) {
184
187
  return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
185
188
  }
186
189
  /**
187
- * The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
188
- * each code's in site order.
190
+ * The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
191
+ * order and each code's in site order.
189
192
  */
190
193
  export const readsAndWrites = (input) => {
191
194
  const run = indexed(input);
@@ -1,10 +1,8 @@
1
+ import { CODE } from "../contract/contract.generated.js";
1
2
  import { REPOSITORY_DOCUMENT } from "../config.js";
2
3
  import { FIXED_SEVERITY_CODES } from "../kinds.js";
3
- import { isDependencyCode, isUnsuppressibleCode, STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
4
- /** The code a configured root that matches nothing is reported under. */
5
- const UNMATCHED_ROOT = "DS1704";
6
- /** The code a configured declaration that names nothing is reported under. */
7
- const UNMATCHED_DECLARATION = "DS1706";
4
+ import { isDependencyCode, isUnsuppressibleCode, } from "../suppress.js";
5
+ import { bytewise } from "../text/compare.js";
8
6
  /** The subject kind of a finding about a configured declaration. */
9
7
  const DECLARATION_SUBJECT = "configured-declaration";
10
8
  /** The subject kind of a finding about a suppression record. */
@@ -63,7 +61,7 @@ function suppressionRef(symbol, path, files) {
63
61
  }
64
62
  /** One finding per refused suppression: the grammar's reason refusal and its scope refusal. */
65
63
  function refusedSuppressions(refusals, files) {
66
- return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === SUPPRESSION_WITHOUT_REASON
64
+ return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === CODE.suppressionWithoutReason
67
65
  ? "carries no reason"
68
66
  : "names a symbol and no path"}`, { mechanism: refused.mechanism, entry: writtenEntry(refused) }));
69
67
  }
@@ -83,10 +81,10 @@ export function staleSuppressions(stale, files) {
83
81
  bySite.set(key, held);
84
82
  }
85
83
  return [...bySite.values()].map(({ first, records }) => {
86
- const codes = [...new Set(records.map((one) => one.record.code))].sort();
84
+ const codes = [...new Set(records.map((one) => one.record.code))].sort(bytewise);
87
85
  const reports = [...new Set(records.flatMap((one) => one.reports))]
88
86
  .filter((code) => !codes.includes(code))
89
- .sort();
87
+ .sort(bytewise);
90
88
  const rows = codes.filter(isUnsuppressibleCode);
91
89
  const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
92
90
  const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
@@ -112,7 +110,7 @@ export function staleSuppressions(stale, files) {
112
110
  ? ""
113
111
  : `, and ${declared}`;
114
112
  return {
115
- code: STALE_SUPPRESSION,
113
+ code: CODE.staleSuppression,
116
114
  mechanism: first.mechanism,
117
115
  entry: {
118
116
  code: first.code,
@@ -141,7 +139,7 @@ function documentPosition(document) {
141
139
  */
142
140
  function unmatchedDeclarationFindings(unmatched) {
143
141
  return unmatched.map((one) => ({
144
- code: UNMATCHED_DECLARATION,
142
+ code: CODE.unmatchedConfiguredDeclaration,
145
143
  position: documentPosition(one.document),
146
144
  symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
147
145
  message: `${one.key} entry names no declaration in any project`,
@@ -157,7 +155,7 @@ function unmatchedDeclarationFindings(unmatched) {
157
155
  export function unmatchedRootFindings(sources, document) {
158
156
  const position = documentPosition(document);
159
157
  return sources.map((source) => ({
160
- code: UNMATCHED_ROOT,
158
+ code: CODE.unmatchedRoot,
161
159
  position,
162
160
  symbol: { ref: source, kind: ROOT_SUBJECT, name: source, sizeLines: 1 },
163
161
  message: source.includes("*") || source.includes("?")
@@ -166,7 +164,7 @@ export function unmatchedRootFindings(sources, document) {
166
164
  }));
167
165
  }
168
166
  /**
169
- * The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
167
+ * The findings of the configuration family, `DS9000` to `DS9099`: a suppression refused for
170
168
  * carrying no reason or naming no path, and a configured root or declaration that named
171
169
  * nothing. A
172
170
  * suppression that matched nothing is a record of the report's stale suppressions, which
@@ -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 family reports: an interface and a method an
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 NOT_THIS_FAMILY = new Set([
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 the family names. */
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, and the unreferenced codes, an unimportable
45
- * unused export being the unreachable kind's.
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 "DS1005";
53
+ return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
51
54
  }
52
- if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
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 "DS1004";
66
+ return CODE.testOnlyUse;
64
67
  }
65
68
  if (candidate.productionRefs === 0 && judged.deprecated) {
66
- return "DS1006";
69
+ return CODE.deprecatedUnused;
67
70
  }
68
- if (candidate.productionRefs === 0 && candidate.testRefs > 0) {
69
- return "DS1004";
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 "DS1003";
77
+ return CODE.unusedMember;
73
78
  }
74
79
  if (!symbol.exported) {
75
- return "DS1002";
80
+ return CODE.unusedUnexported;
76
81
  }
77
- return judged.unreachable ? undefined : "DS1001";
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 "DS1003";
87
+ return CODE.unusedMember;
83
88
  }
84
- return symbol.exported ? "DS1001" : "DS1002";
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 "DS1005":
112
+ case CODE.testOfDeadCode:
108
113
  return TEST_OF_DEAD_CODE;
109
- case "DS1006":
114
+ case CODE.deprecatedUnused:
110
115
  return `deprecated ${word} has no production reference`;
111
- case "DS1004":
112
- return `${word} is referenced only from test files and never from production code`;
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 === "DS1003"
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 === "DS1001") {
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,22 +168,22 @@ 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-declarations family, `DS1000` to `DS1099`. */
171
+ /** The findings of the unused-declaration kinds, `DS1001` to `DS1004` and `DS1102` to `DS1104`. */
165
172
  export const unusedDeclarations = (input) => [
166
- ...familyFindings(input, false),
173
+ ...emitterFindings(input, false),
167
174
  ...unusedImports(input),
168
175
  ];
169
176
  /**
170
- * The import bindings their file never reads, under the unexported code. A generated
171
- * file's are the generator's to remove unless the run includes generated files.
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.
172
179
  */
173
180
  function unusedImports(input) {
174
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
181
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
175
182
  return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
176
183
  ? []
177
184
  : [
178
185
  {
179
- code: "DS1002",
186
+ code: CODE.unusedImport,
180
187
  position: binding.position,
181
188
  symbol: { ref: binding.ref, kind: "import", name: binding.name, sizeLines: 1 },
182
189
  message: `import binding ${binding.name} is never read in its file`,
@@ -188,12 +195,13 @@ function unusedImports(input) {
188
195
  * a record on such a member is matched against, since the report carries the container's
189
196
  * finding rather than one of the member's own.
190
197
  */
191
- export const fallenMembers = (input) => familyFindings(input, true);
192
- /** The family's findings, or, where `fallen` is set, its dead containers' members' alone. */
193
- function familyFindings(input, fallen) {
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) {
194
201
  const union = input.swept.matrix.union;
195
202
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
196
203
  const unreachable = unreachableExports(input);
204
+ const unseenAPI = testsUnseenAPI(input);
197
205
  let brands;
198
206
  const found = [];
199
207
  for (const candidate of input.swept.sweep.candidates) {
@@ -212,10 +220,11 @@ function familyFindings(input, fallen) {
212
220
  test: union.test[at] === true,
213
221
  deprecated: input.deprecated.has(symbol.id),
214
222
  unreachable: unreachable.has(symbol.id),
223
+ testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
215
224
  };
216
225
  const code = codeOf(judged);
217
226
  if (code === undefined ||
218
- (code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))) {
227
+ (code === CODE.unusedMember && (brands ??= brandMembers(input)).has(symbol.id))) {
219
228
  continue;
220
229
  }
221
230
  const endLine = Math.max(symbol.position.line, symbol.endLine);
@@ -1,17 +1,15 @@
1
+ import { CODE } from "../contract/contract.generated.js";
1
2
  import { OUTSIDE } from "../graph.js";
2
3
  import { declarationKey } from "../ref.js";
3
4
  import { runnerCalls } from "../sweep.js";
4
5
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
5
- const UNNECESSARY_EXPORT = "DS1101";
6
- const UNREACHABLE_EXPORT = "DS1103";
7
- const REDUNDANT_EXPORT_KEYWORD = "DS1104";
8
6
  /**
9
7
  * The root kinds by which a consumer importing the package reaches a declaration. A
10
8
  * declaration or a file rooted by any other kind is entered by something that names it
11
9
  * from outside the reference graph: a runtime, a tool, a framework or the configuration.
12
10
  */
13
11
  const PUBLISHING = new Set(["manifest-entry", "published-api"]);
14
- /** The reader's word for each kind of declaration this family reports. */
12
+ /** The reader's word for each kind of declaration this emitter reports. */
15
13
  const WORDS = {
16
14
  function: "function",
17
15
  class: "class",
@@ -34,12 +32,17 @@ const RUN_ONLY = new Set(["configuration-string", "script"]);
34
32
  function entersByName(kinds) {
35
33
  return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
36
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"]);
37
40
  /**
38
41
  * Whether the roots of a declaration's file enter it by name: a test runner's root
39
42
  * enters only an export the runner calls, so another export of a test file is judged.
40
43
  */
41
44
  function fileEntersByName(kinds, symbol) {
42
- const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind));
45
+ const entering = [...(kinds ?? [])].filter((kind) => !PUBLISHING.has(kind) && !RUN_ONLY.has(kind) && !PER_NAME.has(kind));
43
46
  return (entering.some((kind) => kind !== "test-runner") || (entering.length > 0 && runnerCalls(symbol)));
44
47
  }
45
48
  /**
@@ -104,8 +107,8 @@ function wordOf(symbol) {
104
107
  }
105
108
  /** The visibility each narrowing code names, which only the narrowing codes carry. */
106
109
  const NARROWER = {
107
- [REDUNDANT_EXPORT_KEYWORD]: "file",
108
- [UNNECESSARY_EXPORT]: "package",
110
+ [CODE.redundantExportKeyword]: "file",
111
+ [CODE.unnecessaryExport]: "package",
109
112
  };
110
113
  function findingOf(symbol, code, message) {
111
114
  const { path, line, column } = symbol.position;
@@ -148,12 +151,12 @@ function narrowing(facts, at, symbol, world) {
148
151
  if (facts.published[file] === true && !closed) {
149
152
  return undefined;
150
153
  }
151
- return findingOf(symbol, REDUNDANT_EXPORT_KEYWORD, `exported ${word} is referenced only inside the file that declares it`);
154
+ return findingOf(symbol, CODE.redundantExportKeyword, `exported ${word} is referenced only inside the file that declares it`);
152
155
  case "package":
153
156
  if (!closed || !publishes(facts.rootedBy[at])) {
154
157
  return undefined;
155
158
  }
156
- return findingOf(symbol, UNNECESSARY_EXPORT, `exported ${word} is referenced only inside the package that declares it`);
159
+ return findingOf(symbol, CODE.unnecessaryExport, `exported ${word} is referenced only inside the package that declares it`);
157
160
  default:
158
161
  return undefined;
159
162
  }
@@ -163,7 +166,7 @@ function narrowing(facts, at, symbol, world) {
163
166
  * export no code outside the target can import, because the manifest declares
164
167
  * `exports` and no root reaches it. Each arm yields to a more specific kind, so one
165
168
  * declaration is reported once: a test whose every subject is dead, an interface, a
166
- * 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.
167
170
  */
168
171
  function unreachable(facts, at, symbol, candidate, input) {
169
172
  if (!input.boundary.encapsulated ||
@@ -171,17 +174,17 @@ function unreachable(facts, at, symbol, candidate, input) {
171
174
  (facts.rootedBy[at]?.size ?? 0) > 0 ||
172
175
  candidate.testOfDeadCode ||
173
176
  symbol.kind === "interface" ||
174
- (candidate.productionRefs === 0 && candidate.testRefs > 0) ||
177
+ (candidate.testRefs > 0 && (candidate.productionRefs === 0 || candidate.keptByTests)) ||
175
178
  (candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
176
179
  return undefined;
177
180
  }
178
181
  const found = candidate.relation === "reachability"
179
182
  ? "is referenced only from declarations that are themselves dead"
180
183
  : "has no reference in the target";
181
- return findingOf(symbol, UNREACHABLE_EXPORT, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
184
+ return findingOf(symbol, CODE.unreachableExport, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
182
185
  }
183
186
  /**
184
- * The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
187
+ * The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
185
188
  * about a symbol a declared cross-language edge names is reported here like any other, and
186
189
  * the edge's evaluation then holds it pending, because the edge stands for a reference from
187
190
  * outside the file and the package whose use is decided in the other language. A
@@ -190,10 +193,10 @@ function unreachable(facts, at, symbol, candidate, input) {
190
193
  export function narrowings(input) {
191
194
  const facts = factsOf(input.swept.matrix.union);
192
195
  const { declared, loaded } = input.boundary.consumers;
193
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
196
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
194
197
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
195
198
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
196
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
199
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
197
200
  const findings = [];
198
201
  facts.graph.symbols.forEach((symbol, at) => {
199
202
  if (generated?.has(symbol.position.path) === true) {
@@ -229,5 +232,5 @@ export function unreachableExports(input) {
229
232
  }
230
233
  return found;
231
234
  }
232
- /** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
235
+ /** The findings of the visibility-narrowing kinds, `DS1501`, `DS1101` and `DS1503`. */
233
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 { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.js";
11
- /** The exit code of a report holding at least one pending finding. */
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 (compare(a.position.path, b.position.path) ||
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
- compare(a.code, b.code) ||
26
- compare(a.symbol.ref, b.symbol.ref));
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.failOn));
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: "deadset-ts", version };
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.length),
32
- bases: take(contract.bases.length),
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.lifecycleContracts.length === 0) {
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.lifecycleContracts);
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.lifecycleContracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
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
+ }