@cplieger/deadset-ts 8.0.0-dev.7 → 8.0.0-dev.9

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 (137) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +94 -299
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configuration-files.js +2 -9
  5. package/dist/src/configured-declarations.js +15 -20
  6. package/dist/src/consumers.js +3 -30
  7. package/dist/src/contract/config.generated.js +544 -0
  8. package/dist/src/contract/contract.generated.js +20 -10
  9. package/dist/src/contract/report.generated.js +3 -2
  10. package/dist/src/conventions.js +3 -8
  11. package/dist/src/edges.js +36 -112
  12. package/dist/src/exempt-classes.js +0 -2
  13. package/dist/src/exempt.js +1 -1
  14. package/dist/src/failure.js +36 -0
  15. package/dist/src/findings/completion.js +5 -4
  16. package/dist/src/findings/emitters.js +1 -1
  17. package/dist/src/findings/intra-function.js +1 -1
  18. package/dist/src/findings/non-code-artifacts.js +2 -2
  19. package/dist/src/findings/unused-declarations.js +1 -1
  20. package/dist/src/findings/visibility-narrowing.js +2 -2
  21. package/dist/src/findings-pass.js +1 -1
  22. package/dist/src/framework-lifecycle.js +7 -7
  23. package/dist/src/fs/document.js +52 -0
  24. package/dist/src/fs/manifest.js +7 -0
  25. package/dist/src/injection-container.js +1 -1
  26. package/dist/src/json-document.js +194 -67
  27. package/dist/src/kinds.js +0 -7
  28. package/dist/src/load/classify.js +236 -0
  29. package/dist/src/load/diagnostics.js +79 -0
  30. package/dist/src/load/discover.js +230 -0
  31. package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
  32. package/dist/src/load/expected-module.js +136 -0
  33. package/dist/src/load/load.js +22 -0
  34. package/dist/src/load/module-lookup.js +37 -0
  35. package/dist/src/load/plan.js +155 -0
  36. package/dist/src/{test-globals.js → load/test-globals.js} +51 -56
  37. package/dist/src/load/test-joins.js +82 -0
  38. package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +3 -3
  39. package/dist/src/{workspace-programs.js → load/workspace-programs.js} +37 -40
  40. package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +21 -59
  41. package/dist/src/{workspace-run.js → load/workspace-run.js} +4 -3
  42. package/dist/src/{workspace.js → load/workspace.js} +21 -17
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/print.js +10 -79
  45. package/dist/src/report-schema.js +3 -224
  46. package/dist/src/resolve.js +245 -715
  47. package/dist/src/run.js +59 -72
  48. package/dist/src/schema.js +320 -344
  49. package/dist/src/scope.js +37 -113
  50. package/dist/src/serialization-contract.js +1 -1
  51. package/dist/src/session.js +22 -28
  52. package/dist/src/source-files.js +1 -1
  53. package/dist/src/suppress-file.js +15 -17
  54. package/dist/src/suppress.js +3 -3
  55. package/dist/src/template-field.js +4 -4
  56. package/dist/src/type-errors.js +11 -216
  57. package/dist/src/usage.js +10 -0
  58. package/dist/src/verbs/analyze.js +100 -122
  59. package/dist/src/verbs/describe.js +5 -12
  60. package/dist/src/verbs/explain.js +3 -3
  61. package/dist/src/verbs/print-projects.js +2 -2
  62. package/dist/src/verbs/version.js +12 -2
  63. package/jsr.json +1 -1
  64. package/package.json +1 -1
  65. package/src/analysis.ts +95 -460
  66. package/src/config.ts +31 -298
  67. package/src/configuration-files.ts +8 -10
  68. package/src/configured-declarations.ts +18 -23
  69. package/src/consumers.ts +6 -57
  70. package/src/contract/config.generated.ts +798 -0
  71. package/src/contract/contract.generated.ts +20 -10
  72. package/src/contract/report.generated.ts +3 -2
  73. package/src/conventions.ts +6 -12
  74. package/src/edges.ts +39 -124
  75. package/src/exempt-classes.ts +0 -5
  76. package/src/exempt.ts +4 -4
  77. package/src/failure.ts +63 -0
  78. package/src/findings/completion.ts +5 -6
  79. package/src/findings/emitter.ts +2 -2
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +1 -1
  82. package/src/findings/non-code-artifacts.ts +4 -4
  83. package/src/findings/unused-declarations.ts +1 -1
  84. package/src/findings/visibility-narrowing.ts +2 -2
  85. package/src/findings-pass.ts +3 -3
  86. package/src/framework-lifecycle.ts +7 -7
  87. package/src/fs/document.ts +70 -0
  88. package/src/fs/manifest.ts +8 -0
  89. package/src/host.ts +12 -5
  90. package/src/injection-container.ts +1 -1
  91. package/src/json-document.ts +236 -76
  92. package/src/kinds.ts +0 -8
  93. package/src/load/classify.ts +352 -0
  94. package/src/load/diagnostics.ts +141 -0
  95. package/src/load/discover.ts +378 -0
  96. package/src/{emit-map.ts → load/emit-map.ts} +1 -1
  97. package/src/load/expected-module.ts +188 -0
  98. package/src/load/load.ts +61 -0
  99. package/src/load/module-lookup.ts +50 -0
  100. package/src/load/plan.ts +258 -0
  101. package/src/{test-globals.ts → load/test-globals.ts} +59 -65
  102. package/src/load/test-joins.ts +98 -0
  103. package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +4 -4
  104. package/src/{workspace-programs.ts → load/workspace-programs.ts} +59 -53
  105. package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +31 -78
  106. package/src/{workspace-run.ts → load/workspace-run.ts} +14 -11
  107. package/src/{workspace.ts → load/workspace.ts} +28 -21
  108. package/src/outside-tests.ts +1 -1
  109. package/src/partial.ts +1 -1
  110. package/src/print.ts +11 -92
  111. package/src/report-schema.ts +3 -299
  112. package/src/resolve.ts +281 -1105
  113. package/src/run.ts +68 -80
  114. package/src/schema.ts +391 -389
  115. package/src/scope.ts +45 -131
  116. package/src/serialization-contract.ts +1 -1
  117. package/src/session.ts +52 -53
  118. package/src/source-files.ts +1 -1
  119. package/src/suppress-file.ts +15 -20
  120. package/src/suppress.ts +3 -3
  121. package/src/template-field.ts +10 -7
  122. package/src/type-errors.ts +18 -288
  123. package/src/usage.ts +10 -0
  124. package/src/verbs/analyze.ts +139 -157
  125. package/src/verbs/describe.ts +4 -11
  126. package/src/verbs/explain.ts +3 -7
  127. package/src/verbs/print-projects.ts +2 -2
  128. package/src/verbs/verb.ts +2 -2
  129. package/src/verbs/version.ts +13 -2
  130. package/dist/src/base-url.js +0 -18
  131. package/dist/src/discover.js +0 -374
  132. package/dist/src/setup-failure.js +0 -19
  133. package/dist/src/test-joins.js +0 -101
  134. package/src/base-url.ts +0 -24
  135. package/src/discover.ts +0 -548
  136. package/src/setup-failure.ts +0 -37
  137. package/src/test-joins.ts +0 -118
package/dist/src/edges.js CHANGED
@@ -4,144 +4,68 @@
4
4
  * never resolves the paired symbol: the other language's analyzer decides whether the
5
5
  * pairing is a use, and the merge reads the two evaluations together.
6
6
  */
7
- import { ConfigError } from "./config.js";
8
- import { isRecord, positionAt, walkDocument } from "./json-document.js";
9
- import { joinPath } from "./paths.js";
7
+ import { EDGES_SCHEMA } from "./contract/config.generated.js";
8
+ import { readTargetDocument } from "./fs/document.js";
9
+ import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.js";
10
10
  import { positionKey } from "./position.js";
11
+ import { firstRefusal } from "./schema.js";
11
12
  import { isPartSubject } from "./suppress.js";
12
13
  import { bytewise } from "./text/compare.js";
13
- import { message } from "./product.js";
14
+ import { UsageError } from "./usage.js";
14
15
  /** The name and the location the Contract fixes for the edges document. */
15
16
  export const EDGES_FILE = "deadset-edges.json";
16
17
  /** The language prefix of every reference this analyzer evaluates. */
17
18
  const OWN_PREFIX = "ts://";
18
- /** The closed key list of the document and of one edge. */
19
- const DOCUMENT_KEYS = new Set(["description", "edges"]);
20
- const EDGE_KEYS = new Set(["id", "because", "provides", "used_by"]);
21
- /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
22
- const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
23
- /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
24
- const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
25
- const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
26
- const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
27
- const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
28
- const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
29
19
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
30
- class EdgesError extends ConfigError {
31
- constructor(at, text, want) {
32
- super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
20
+ class EdgesError extends UsageError {
21
+ constructor(at, reason) {
22
+ super(`${at}: ${reason}`);
33
23
  this.name = "EdgesError";
34
24
  }
35
25
  }
36
- /**
37
- * Whether a reference carries a wildcard outside a quoted or a computed component, which
38
- * would name a set of symbols rather than one.
39
- */
40
- function carriesWildcard(ref) {
41
- let quoted = false;
42
- let computed = false;
43
- let escaped = false;
44
- for (const char of ref) {
45
- if (escaped) {
46
- escaped = false;
47
- }
48
- else if (quoted && char === "\\") {
49
- escaped = true;
50
- }
51
- else if (quoted) {
52
- quoted = char !== "'";
53
- }
54
- else if (computed) {
55
- computed = char !== "]";
56
- }
57
- else if (char === "'") {
58
- quoted = true;
59
- }
60
- else if (char === "[") {
61
- computed = true;
62
- }
63
- else if (char === "*") {
64
- return true;
65
- }
66
- }
67
- return false;
68
- }
69
- /** One edge, checked: the closed key list, the value types and the value forms. */
70
- function readEdge(value, at) {
71
- const refuse = (text, want) => {
72
- throw new EdgesError(at, text, want);
73
- };
74
- if (!isRecord(value)) {
75
- return refuse(JSON.stringify(value), EDGE_WANT);
76
- }
77
- for (const [key, held] of Object.entries(value)) {
78
- if (!EDGE_KEYS.has(key) || typeof held !== "string") {
79
- refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
80
- }
81
- }
82
- const edge = value;
83
- const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
84
- const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
85
- const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
86
- if (!ID_FORM.test(id)) {
87
- refuse(JSON.stringify(id), ID_WANT);
88
- }
89
- for (const ref of [provides, usedBy]) {
90
- if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
91
- refuse(JSON.stringify(ref), REFERENCE_WANT);
92
- }
93
- }
94
- return { id, provides, usedBy };
95
- }
26
+ /** The sides of one edge, in the order a reader yields them. */
27
+ const SIDE_NAMES = ["provides", "used_by"];
96
28
  /**
97
29
  * Every side of a declared edge that names a symbol of this language, in document order,
98
30
  * `provides` before `used_by` within an edge. An absent document declares no edge. The
99
- * document is strict JSON with a closed key list at both levels, and a defect in it is an
100
- * {@link EdgesError}.
31
+ * document is strict JSON held to the Contract's edges schema, and a defect in it is an
32
+ * {@link EdgesError} at the line and column of the edge it is in, where it is in one.
101
33
  */
102
34
  export function readEdgeSides(host, targetRoot) {
103
- const file = joinPath(targetRoot, EDGES_FILE);
104
- if (host.kindOf(file) === "absent") {
35
+ const text = readTargetDocument(host, targetRoot, EDGES_FILE);
36
+ if (text === undefined) {
105
37
  return [];
106
38
  }
107
- const text = host.readFile(file);
108
- let parsed;
39
+ let read;
109
40
  try {
110
- parsed = JSON.parse(text);
41
+ read = readStrict(text, "edges");
111
42
  }
112
43
  catch (error) {
113
- throw new EdgesError(EDGES_FILE, message(error), DOCUMENT_WANT);
114
- }
115
- if (!isRecord(parsed)) {
116
- throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
117
- }
118
- const walk = walkDocument(text, "edges");
119
- if (walk.twice !== undefined) {
120
- throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)), `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`, DOCUMENT_WANT);
44
+ if (!(error instanceof StrictJSONError)) {
45
+ throw error;
46
+ }
47
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
121
48
  }
122
- const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
123
- const description = parsed["description"];
124
- const edges = parsed["edges"];
125
- if (unknown !== undefined ||
126
- (description !== undefined && typeof description !== "string") ||
127
- !Array.isArray(edges)) {
128
- throw new EdgesError(EDGES_FILE, unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`, DOCUMENT_WANT);
49
+ const value = read.value;
50
+ const refused = firstRefusal(EDGES_SCHEMA, value);
51
+ if (refused !== undefined) {
52
+ const place = placeIn(value, refused.pointer, refused.member);
53
+ const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
54
+ const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
55
+ const where = dotted(place);
56
+ throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
129
57
  }
58
+ const edges = value.edges;
130
59
  const sides = [];
131
- edges.forEach((value, index) => {
132
- const offset = walk.starts[index];
133
- const at = offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
134
- const edge = readEdge(value, at);
135
- const named = [
136
- ["provides", edge.provides],
137
- ["used_by", edge.usedBy],
138
- ];
139
- for (const [side, symbol] of named) {
140
- if (symbol.startsWith(OWN_PREFIX)) {
141
- sides.push({ edge: edge.id, side, symbol });
60
+ for (const id of read.starts.keys()) {
61
+ const edge = String(id);
62
+ for (const side of SIDE_NAMES) {
63
+ const symbol = edges[edge]?.[side];
64
+ if (symbol?.startsWith(OWN_PREFIX) === true) {
65
+ sides.push({ edge, side, symbol });
142
66
  }
143
67
  }
144
- });
68
+ }
145
69
  return sides;
146
70
  }
147
71
  /**
@@ -11,5 +11,3 @@ const VISIBILITY = new Map(TS_ROWS.map((row) => [row.class, row.typescriptVisibi
11
11
  export function typescriptVisibilityOf(exemptionClass) {
12
12
  return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
13
13
  }
14
- /** Every class of the vocabulary, whichever language it runs on, in the vocabulary's order. */
15
- export const EXEMPTION_CLASS_NAMES = EXEMPTION_CLASSES.map((row) => row.class);
@@ -17,7 +17,7 @@ import { bytewise } from "./text/compare.js";
17
17
  * only switches nothing off here, so one configuration serves every analyzer.
18
18
  */
19
19
  export function disabledClasses(config) {
20
- return new Set(config.exemptionsDisabled);
20
+ return new Set(config.exemptions.disabled);
21
21
  }
22
22
  /**
23
23
  * Whether a class whose row states `allowed` may retain a member of one visibility. A
@@ -0,0 +1,36 @@
1
+ import { FAILURE_CLASSES, FAILURE_PREFIXES } from "./contract/contract.generated.js";
2
+ import { PRODUCT } from "./product.js";
3
+ /** The failure as a class line of the error stream, opened by its group's prefix. */
4
+ export function failureLine(failure) {
5
+ return `${FAILURE_PREFIXES[FAILURE_CLASSES[failure.failureClass]]}${failure.failureClass}: ${failure.detail}`;
6
+ }
7
+ /**
8
+ * The lines of the error stream that name the failures: the first one's class line, the run's
9
+ * only one, then each further failure as its class and detail with no group prefix.
10
+ */
11
+ export function failureLines([first, ...further]) {
12
+ return [failureLine(first), ...further.map((one) => `${one.failureClass}: ${one.detail}`)];
13
+ }
14
+ /**
15
+ * A run the analysis cannot answer. It ends the run with the failure code and no finding
16
+ * list, printing the compiler's located errors and then the failures' lines.
17
+ */
18
+ export class FailureExit extends Error {
19
+ failures;
20
+ /** The compiler's errors behind the failures, in the order it reported them. */
21
+ diagnostics;
22
+ constructor(failures, diagnostics = []) {
23
+ super(failureLines(failures).join("\n"));
24
+ this.name = "FailureExit";
25
+ this.failures = failures;
26
+ this.diagnostics = diagnostics;
27
+ }
28
+ }
29
+ /** A failure exit of one failure. */
30
+ export function failureExit(failureClass, detail, diagnostics = []) {
31
+ return new FailureExit([{ failureClass, detail }], diagnostics);
32
+ }
33
+ /** A fault of this analyzer itself, `fault` saying what it cannot do. */
34
+ export function productFault(fault) {
35
+ return failureExit("product-fault", `${PRODUCT} ${fault}: reinstall ${PRODUCT}, and report the fault to its maintainers if a reinstalled ${PRODUCT} fails the same way`);
36
+ }
@@ -41,8 +41,9 @@ function everyDeclaredLoaded(boundary) {
41
41
  * naming its family, else the code's default.
42
42
  */
43
43
  function severityOf(input, code, fallback) {
44
- const { config } = input;
45
- return (config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback);
44
+ const { severity } = input.config;
45
+ const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
46
+ return key === undefined ? fallback : (severity[key] ?? fallback);
46
47
  }
47
48
  /**
48
49
  * The declarations the roots name as a library's published API, each public member of a
@@ -165,7 +166,7 @@ function testUsedSubjects(input) {
165
166
  function rootsCeiling(input, finding, candidate, configurations) {
166
167
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
167
168
  const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
168
- const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
169
+ const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
169
170
  return unreachable && !complete ? "probable" : "certain";
170
171
  }
171
172
  /**
@@ -460,7 +461,7 @@ function dialed(finding, minConfidence) {
460
461
  * root withholds itself alone.
461
462
  */
462
463
  export function dialsOf(input, findings) {
463
- const minConfidence = input.config.analysis.minConfidence;
464
+ const minConfidence = input.config.analysis.min_confidence;
464
465
  const components = new Set(findings
465
466
  .filter((finding) => finding.component.root &&
466
467
  dialed({ ...finding, confidence: componentConfidenceOf(finding) }, minConfidence))
@@ -86,7 +86,7 @@ function atPossible(input) {
86
86
  const { config } = input;
87
87
  return {
88
88
  ...input,
89
- config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
89
+ config: { ...config, analysis: { ...config.analysis, min_confidence: "possible" } },
90
90
  };
91
91
  }
92
92
  /**
@@ -98,7 +98,7 @@ export const intraFunction = (input) => {
98
98
  return symbol === undefined ? [] : [symbol.id];
99
99
  }));
100
100
  const { declared, loaded } = input.boundary.consumers;
101
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
101
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
102
102
  const published = (id) => {
103
103
  const symbol = byId.get(id);
104
104
  return (symbol !== undefined &&
@@ -10,8 +10,8 @@ const FILE_START = 1;
10
10
  * derived matrix is never complete.
11
11
  */
12
12
  function completeMatrix(config) {
13
- return (config.analysis.matrixComplete &&
14
- config.analysis.configurations.some((entry) => entry.shape === "project"));
13
+ return (config.analysis.matrix.complete &&
14
+ config.analysis.configurations.some((entry) => "project" in entry));
15
15
  }
16
16
  /** One finding about a whole file. */
17
17
  function fileFinding(code, file, message) {
@@ -178,7 +178,7 @@ export const unusedDeclarations = (input) => [
178
178
  * remove unless the run includes generated files.
179
179
  */
180
180
  function unusedImports(input) {
181
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
181
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
182
182
  return input.intraFunction.imports.flatMap((binding) => binding.used || generated?.has(binding.position.path) === true
183
183
  ? []
184
184
  : [
@@ -193,10 +193,10 @@ function unreachable(facts, at, symbol, candidate, input) {
193
193
  export function narrowings(input) {
194
194
  const facts = factsOf(input.swept.matrix.union);
195
195
  const { declared, loaded } = input.boundary.consumers;
196
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
196
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
197
197
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
198
198
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
199
- const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
199
+ const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
200
200
  const findings = [];
201
201
  facts.graph.symbols.forEach((symbol, at) => {
202
202
  if (generated?.has(symbol.position.path) === true) {
@@ -66,7 +66,7 @@ export function verdictOf(result, config) {
66
66
  if (result.totals.staleSuppressions > 0) {
67
67
  return EXIT_FINDINGS;
68
68
  }
69
- 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));
70
70
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
71
71
  }
72
72
  /**
@@ -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
+ }
@@ -4,6 +4,13 @@ import { joinPath } from "../paths.js";
4
4
  export const MANIFEST = "package.json";
5
5
  /** The directory a package manager installs packages into, whose files are no project's own. */
6
6
  export const INSTALLED_PACKAGES = "node_modules";
7
+ /** The manifest members a package declares the packages it depends on under. */
8
+ export const DEPENDENCY_SECTIONS = [
9
+ "dependencies",
10
+ "devDependencies",
11
+ "peerDependencies",
12
+ "optionalDependencies",
13
+ ];
7
14
  /** Whether one path passes through an installed-packages directory. */
8
15
  export function inInstalledPackages(path) {
9
16
  return path.split("/").includes(INSTALLED_PACKAGES);
@@ -24,7 +24,7 @@ function isInjected(node) {
24
24
  /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
25
25
  function registrations(input) {
26
26
  const { project, held } = input;
27
- const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
27
+ const resolved = resolveEntries(project, held, input.ts.injection_registrations);
28
28
  if (!resolved.some(namesADeclaration)) {
29
29
  return [];
30
30
  }