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

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 (78) hide show
  1. package/dist/bin/node-host.js +21 -3
  2. package/dist/src/analysis.js +13 -14
  3. package/dist/src/config.js +6 -83
  4. package/dist/src/configured-declarations.js +15 -20
  5. package/dist/src/consumers.js +2 -2
  6. package/dist/src/contract/config.generated.js +544 -0
  7. package/dist/src/contract/report.generated.js +3 -2
  8. package/dist/src/discover.js +1 -1
  9. package/dist/src/edges.js +36 -112
  10. package/dist/src/exempt-classes.js +0 -2
  11. package/dist/src/exempt.js +1 -1
  12. package/dist/src/findings/completion.js +5 -4
  13. package/dist/src/findings/emitters.js +1 -1
  14. package/dist/src/findings/intra-function.js +1 -1
  15. package/dist/src/findings/non-code-artifacts.js +2 -2
  16. package/dist/src/findings/unused-declarations.js +1 -1
  17. package/dist/src/findings/visibility-narrowing.js +2 -2
  18. package/dist/src/findings-pass.js +1 -1
  19. package/dist/src/framework-lifecycle.js +7 -7
  20. package/dist/src/fs/document.js +59 -0
  21. package/dist/src/injection-container.js +1 -1
  22. package/dist/src/json-document.js +194 -67
  23. package/dist/src/kinds.js +0 -7
  24. package/dist/src/print.js +10 -79
  25. package/dist/src/report-schema.js +3 -224
  26. package/dist/src/resolve.js +245 -715
  27. package/dist/src/run.js +48 -55
  28. package/dist/src/schema.js +320 -344
  29. package/dist/src/scope.js +33 -101
  30. package/dist/src/serialization-contract.js +1 -1
  31. package/dist/src/suppress-file.js +15 -17
  32. package/dist/src/suppress.js +3 -3
  33. package/dist/src/template-field.js +4 -4
  34. package/dist/src/usage.js +10 -0
  35. package/dist/src/verbs/analyze.js +12 -15
  36. package/dist/src/verbs/explain.js +3 -3
  37. package/dist/src/verbs/print-projects.js +2 -2
  38. package/jsr.json +1 -1
  39. package/package.json +1 -1
  40. package/src/analysis.ts +30 -26
  41. package/src/config.ts +31 -298
  42. package/src/configured-declarations.ts +18 -23
  43. package/src/consumers.ts +4 -4
  44. package/src/contract/config.generated.ts +798 -0
  45. package/src/contract/report.generated.ts +3 -2
  46. package/src/discover.ts +1 -1
  47. package/src/edges.ts +39 -124
  48. package/src/exempt-classes.ts +0 -5
  49. package/src/exempt.ts +4 -4
  50. package/src/findings/completion.ts +5 -6
  51. package/src/findings/emitter.ts +2 -2
  52. package/src/findings/emitters.ts +1 -1
  53. package/src/findings/intra-function.ts +1 -1
  54. package/src/findings/non-code-artifacts.ts +4 -4
  55. package/src/findings/unused-declarations.ts +1 -1
  56. package/src/findings/visibility-narrowing.ts +2 -2
  57. package/src/findings-pass.ts +3 -3
  58. package/src/framework-lifecycle.ts +7 -7
  59. package/src/fs/document.ts +74 -0
  60. package/src/host.ts +12 -5
  61. package/src/injection-container.ts +1 -1
  62. package/src/json-document.ts +236 -76
  63. package/src/kinds.ts +0 -8
  64. package/src/print.ts +11 -92
  65. package/src/report-schema.ts +3 -299
  66. package/src/resolve.ts +281 -1105
  67. package/src/run.ts +50 -63
  68. package/src/schema.ts +391 -389
  69. package/src/scope.ts +39 -119
  70. package/src/serialization-contract.ts +1 -1
  71. package/src/suppress-file.ts +15 -20
  72. package/src/suppress.ts +3 -3
  73. package/src/template-field.ts +10 -7
  74. package/src/usage.ts +10 -0
  75. package/src/verbs/analyze.ts +12 -23
  76. package/src/verbs/explain.ts +3 -7
  77. package/src/verbs/print-projects.ts +2 -2
  78. package/src/verbs/verb.ts +2 -2
@@ -589,7 +589,7 @@ export const REPORT_SCHEMAS = {
589
589
  additionalProperties: false,
590
590
  required: ["edge", "side", "symbol", "state"],
591
591
  properties: {
592
- edge: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
592
+ edge: { $ref: "finding.schema.json#/$defs/edge_identifier" },
593
593
  side: { type: "string", enum: ["provides", "used_by"] },
594
594
  symbol: { type: "string" },
595
595
  state: { type: "string", enum: ["live", "dead", "absent"] },
@@ -969,7 +969,7 @@ export const REPORT_SCHEMAS = {
969
969
  uniqueItems: true,
970
970
  items: { type: "string", minLength: 1 },
971
971
  },
972
- edge: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
972
+ edge: { $ref: "#/$defs/edge_identifier" },
973
973
  sides: {
974
974
  type: "array",
975
975
  minItems: 1,
@@ -1125,6 +1125,7 @@ export const REPORT_SCHEMAS = {
1125
1125
  },
1126
1126
  ],
1127
1127
  $defs: {
1128
+ edge_identifier: { type: "string", pattern: "^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$" },
1128
1129
  position: {
1129
1130
  type: "object",
1130
1131
  additionalProperties: false,
package/src/discover.ts CHANGED
@@ -504,7 +504,7 @@ export function discoverProjects(
504
504
  configurations: readonly BuildConfiguration[] = [],
505
505
  ): Discovery {
506
506
  const declared = configurations.filter(
507
- (entry): entry is ProjectConfiguration => entry.shape === "project",
507
+ (entry): entry is ProjectConfiguration => "project" in entry,
508
508
  );
509
509
  if (declared.length > 0) {
510
510
  return declaredProjects(engine, host, scope.target.path, declared);
package/src/edges.ts CHANGED
@@ -5,17 +5,18 @@
5
5
  * pairing is a use, and the merge reads the two evaluations together.
6
6
  */
7
7
 
8
- import { ConfigError } from "./config.ts";
8
+ import { EDGES_SCHEMA } from "./contract/config.generated.ts";
9
9
  import type { Finding } from "./finding.ts";
10
10
  import type { EdgeSide, EdgeSideName } from "./findings/boundary.ts";
11
+ import { readTargetDocument } from "./fs/document.ts";
11
12
  import type { Host } from "./host.ts";
12
13
  import type { InventorySymbol } from "./inventory.ts";
13
- import { isRecord, positionAt, walkDocument } from "./json-document.ts";
14
- import { joinPath } from "./paths.ts";
14
+ import { dotted, placeIn, positionAt, readStrict, StrictJSONError } from "./json-document.ts";
15
15
  import { positionKey } from "./position.ts";
16
+ import { firstRefusal } from "./schema.ts";
16
17
  import { isPartSubject } from "./suppress.ts";
17
18
  import { bytewise } from "./text/compare.ts";
18
- import { message } from "./product.ts";
19
+ import { UsageError } from "./usage.ts";
19
20
 
20
21
  /** The name and the location the Contract fixes for the edges document. */
21
22
  export const EDGES_FILE = "deadset-edges.json";
@@ -23,147 +24,61 @@ export const EDGES_FILE = "deadset-edges.json";
23
24
  /** The language prefix of every reference this analyzer evaluates. */
24
25
  const OWN_PREFIX = "ts://";
25
26
 
26
- /** The closed key list of the document and of one edge. */
27
- const DOCUMENT_KEYS: ReadonlySet<string> = new Set(["description", "edges"]);
28
- const EDGE_KEYS: ReadonlySet<string> = new Set(["id", "because", "provides", "used_by"]);
29
-
30
- /** An edge identifier: solidus-separated segments of ASCII letters, digits, `_`, `.` and `-`. */
31
- const ID_FORM = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u;
32
-
33
- /** A reference naming one symbol of some language: `<language>://<scope>#<fragment>`. */
34
- const REFERENCE_FORM = /^[a-z][a-z0-9]*:\/\/[^ \t\r\n#]+#[^ \t\r\n]*$/u;
35
-
36
- const DOCUMENT_WANT = "an object carrying the edges array, and an optional description";
37
- const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and an optional because";
38
- const ID_WANT =
39
- "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
40
- const REFERENCE_WANT =
41
- "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
42
-
43
27
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
44
- class EdgesError extends ConfigError {
45
- constructor(at: string, text: string, want: string) {
46
- super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
28
+ class EdgesError extends UsageError {
29
+ constructor(at: string, reason: string) {
30
+ super(`${at}: ${reason}`);
47
31
  this.name = "EdgesError";
48
32
  }
49
33
  }
50
34
 
51
- /**
52
- * Whether a reference carries a wildcard outside a quoted or a computed component, which
53
- * would name a set of symbols rather than one.
54
- */
55
- function carriesWildcard(ref: string): boolean {
56
- let quoted = false;
57
- let computed = false;
58
- let escaped = false;
59
- for (const char of ref) {
60
- if (escaped) {
61
- escaped = false;
62
- } else if (quoted && char === "\\") {
63
- escaped = true;
64
- } else if (quoted) {
65
- quoted = char !== "'";
66
- } else if (computed) {
67
- computed = char !== "]";
68
- } else if (char === "'") {
69
- quoted = true;
70
- } else if (char === "[") {
71
- computed = true;
72
- } else if (char === "*") {
73
- return true;
74
- }
75
- }
76
- return false;
77
- }
78
-
79
- /** One edge, checked: the closed key list, the value types and the value forms. */
80
- function readEdge(value: unknown, at: string): { id: string; provides: string; usedBy: string } {
81
- const refuse = (text: string, want: string): never => {
82
- throw new EdgesError(at, text, want);
83
- };
84
- if (!isRecord(value)) {
85
- return refuse(JSON.stringify(value), EDGE_WANT);
86
- }
87
- for (const [key, held] of Object.entries(value)) {
88
- if (!EDGE_KEYS.has(key) || typeof held !== "string") {
89
- refuse(`the member ${JSON.stringify(key)} holding ${JSON.stringify(held)}`, EDGE_WANT);
90
- }
91
- }
92
- const edge = value as Readonly<Record<string, string | undefined>>;
93
- const id = edge["id"] ?? refuse("an edge naming no id", EDGE_WANT);
94
- const provides = edge["provides"] ?? refuse("an edge naming no provides side", EDGE_WANT);
95
- const usedBy = edge["used_by"] ?? refuse("an edge naming no used_by side", EDGE_WANT);
96
- if (!ID_FORM.test(id)) {
97
- refuse(JSON.stringify(id), ID_WANT);
98
- }
99
- for (const ref of [provides, usedBy]) {
100
- if (!REFERENCE_FORM.test(ref) || carriesWildcard(ref)) {
101
- refuse(JSON.stringify(ref), REFERENCE_WANT);
102
- }
103
- }
104
- return { id, provides, usedBy };
105
- }
35
+ /** The sides of one edge, in the order a reader yields them. */
36
+ const SIDE_NAMES: readonly EdgeSideName[] = ["provides", "used_by"];
106
37
 
107
38
  /**
108
39
  * Every side of a declared edge that names a symbol of this language, in document order,
109
40
  * `provides` before `used_by` within an edge. An absent document declares no edge. The
110
- * document is strict JSON with a closed key list at both levels, and a defect in it is an
111
- * {@link EdgesError}.
41
+ * document is strict JSON held to the Contract's edges schema, and a defect in it is an
42
+ * {@link EdgesError} at the line and column of the edge it is in, where it is in one.
112
43
  */
113
44
  export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide[] {
114
- const file = joinPath(targetRoot, EDGES_FILE);
115
- if (host.kindOf(file) === "absent") {
45
+ const text = readTargetDocument(host, targetRoot, EDGES_FILE);
46
+ if (text === undefined) {
116
47
  return [];
117
48
  }
118
- const text = host.readFile(file);
119
- let parsed: unknown;
49
+ let read;
120
50
  try {
121
- parsed = JSON.parse(text);
51
+ read = readStrict(text, "edges");
122
52
  } catch (error: unknown) {
123
- throw new EdgesError(EDGES_FILE, message(error), DOCUMENT_WANT);
124
- }
125
- if (!isRecord(parsed)) {
126
- throw new EdgesError(EDGES_FILE, JSON.stringify(parsed), DOCUMENT_WANT);
127
- }
128
- const walk = walkDocument(text, "edges");
129
- if (walk.twice !== undefined) {
130
- throw new EdgesError(
131
- positionKey(positionAt(text, EDGES_FILE, walk.twice.offset)),
132
- `the member ${JSON.stringify(walk.twice.name)} is written twice, so neither value is chosen`,
133
- DOCUMENT_WANT,
134
- );
53
+ if (!(error instanceof StrictJSONError)) {
54
+ throw error;
55
+ }
56
+ throw new EdgesError(positionKey(positionAt(text, EDGES_FILE, error.offset)), error.message);
135
57
  }
136
- const unknown = Object.keys(parsed).find((key) => !DOCUMENT_KEYS.has(key));
137
- const description = parsed["description"];
138
- const edges = parsed["edges"];
139
- if (
140
- unknown !== undefined ||
141
- (description !== undefined && typeof description !== "string") ||
142
- !Array.isArray(edges)
143
- ) {
144
- throw new EdgesError(
145
- EDGES_FILE,
146
- unknown === undefined ? EDGES_FILE : `the member ${JSON.stringify(unknown)}`,
147
- DOCUMENT_WANT,
148
- );
58
+ const value = read.value;
59
+ const refused = firstRefusal(EDGES_SCHEMA, value);
60
+ if (refused !== undefined) {
61
+ const place = placeIn(value, refused.pointer, refused.member);
62
+ const offset = place.length > 1 ? read.starts.get(place[1] ?? "") : undefined;
63
+ const at =
64
+ offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
65
+ const where = dotted(place);
66
+ throw new EdgesError(at, where === "" ? refused.reason : `${where}: ${refused.reason}`);
149
67
  }
68
+ const edges = (
69
+ value as { readonly edges: Readonly<Record<string, Record<EdgeSideName, string>>> }
70
+ ).edges;
150
71
 
151
72
  const sides: EdgeSide[] = [];
152
- (edges as readonly unknown[]).forEach((value, index) => {
153
- const offset = walk.starts[index];
154
- const at =
155
- offset === undefined ? EDGES_FILE : positionKey(positionAt(text, EDGES_FILE, offset));
156
- const edge = readEdge(value, at);
157
- const named: readonly [EdgeSideName, string][] = [
158
- ["provides", edge.provides],
159
- ["used_by", edge.usedBy],
160
- ];
161
- for (const [side, symbol] of named) {
162
- if (symbol.startsWith(OWN_PREFIX)) {
163
- sides.push({ edge: edge.id, side, symbol });
73
+ for (const id of read.starts.keys()) {
74
+ const edge = String(id);
75
+ for (const side of SIDE_NAMES) {
76
+ const symbol = edges[edge]?.[side];
77
+ if (symbol?.startsWith(OWN_PREFIX) === true) {
78
+ sides.push({ edge, side, symbol });
164
79
  }
165
80
  }
166
- });
81
+ }
167
82
  return sides;
168
83
  }
169
84
 
@@ -47,8 +47,3 @@ const VISIBILITY: ReadonlyMap<TSExemptionClass, TypeScriptVisibility> = new Map(
47
47
  export function typescriptVisibilityOf(exemptionClass: TSExemptionClass): TypeScriptVisibility {
48
48
  return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
49
49
  }
50
-
51
- /** Every class of the vocabulary, whichever language it runs on, in the vocabulary's order. */
52
- export const EXEMPTION_CLASS_NAMES: readonly ExemptionClass[] = EXEMPTION_CLASSES.map(
53
- (row) => row.class,
54
- );
package/src/exempt.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { EXEMPTION_CLASSES } from "./contract/contract.generated.ts";
11
- import type { Config, TSSection } from "./config.ts";
11
+ import type { ResolvedConfig } from "./config.ts";
12
12
  import {
13
13
  TS_EXEMPTION_CLASSES,
14
14
  typescriptVisibilityOf,
@@ -48,7 +48,7 @@ export interface DetectorInput<Brand> {
48
48
  /** The configured templates, read once for the run. */
49
49
  readonly templates: Templates;
50
50
  /** The `ts` section, whose declarations the configured classes read. */
51
- readonly ts: TSSection;
51
+ readonly ts: ResolvedConfig["ts"];
52
52
  /** The absolute path of each declared consumer, whose modules are part of the analysis. */
53
53
  readonly consumers: readonly string[];
54
54
  /** The project's generated files, by path below the target root, each with the detail its retentions name. */
@@ -75,8 +75,8 @@ interface Holding {
75
75
  * The classes `exemptions.disabled` switches off. A class that runs on another language
76
76
  * only switches nothing off here, so one configuration serves every analyzer.
77
77
  */
78
- export function disabledClasses(config: Config): ReadonlySet<ExemptionClass> {
79
- return new Set(config.exemptionsDisabled);
78
+ export function disabledClasses(config: ResolvedConfig): ReadonlySet<ExemptionClass> {
79
+ return new Set(config.exemptions.disabled);
80
80
  }
81
81
 
82
82
  /**
@@ -65,10 +65,9 @@ function everyDeclaredLoaded(boundary: Boundary): boolean {
65
65
  * naming its family, else the code's default.
66
66
  */
67
67
  function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
68
- const { config } = input;
69
- return (
70
- config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
71
- );
68
+ const { severity } = input.config;
69
+ const key = [code, code.slice(0, FAMILY_KEY_LENGTH)].find((one) => Object.hasOwn(severity, one));
70
+ return key === undefined ? fallback : (severity[key] ?? fallback);
72
71
  }
73
72
 
74
73
  /**
@@ -225,7 +224,7 @@ function rootsCeiling(
225
224
  ): Confidence {
226
225
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
227
226
  const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
228
- const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
227
+ const complete = input.config.ts.roots_complete && !configurations.some((one) => unseen.has(one));
229
228
  return unreachable && !complete ? "probable" : "certain";
230
229
  }
231
230
 
@@ -585,7 +584,7 @@ export function dialsOf(
585
584
  input: EmitterInput,
586
585
  findings: readonly CompletedFinding[],
587
586
  ): Dials<CompletedFinding> {
588
- const minConfidence = input.config.analysis.minConfidence;
587
+ const minConfidence = input.config.analysis.min_confidence;
589
588
  const components = new Set(
590
589
  findings
591
590
  .filter(
@@ -1,5 +1,5 @@
1
1
  import type { RunSweep } from "../analysis.ts";
2
- import type { Config } from "../config.ts";
2
+ import type { ResolvedConfig } from "../config.ts";
3
3
  import type { Dependencies } from "../dependencies.ts";
4
4
  import type { FileFacts } from "../file-facts.ts";
5
5
  import type { Finding } from "../finding.ts";
@@ -14,7 +14,7 @@ import type { SelfCheckFacts } from "./self-check.ts";
14
14
  * the facts about the run each emitter reads beside the sweep.
15
15
  */
16
16
  export interface EmitterInput {
17
- readonly config: Config;
17
+ readonly config: ResolvedConfig;
18
18
  /** The production sweep a report is built from, beside the matrix it judged. */
19
19
  readonly swept: RunSweep;
20
20
  /** The declarations carrying the deprecation marker in any configuration of the run. */
@@ -145,7 +145,7 @@ function atPossible(input: EmitterInput): EmitterInput {
145
145
  const { config } = input;
146
146
  return {
147
147
  ...input,
148
- config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
148
+ config: { ...config, analysis: { ...config.analysis, min_confidence: "possible" } },
149
149
  };
150
150
  }
151
151
 
@@ -127,7 +127,7 @@ export const intraFunction: Emitter = (input) => {
127
127
  }),
128
128
  );
129
129
  const { declared, loaded } = input.boundary.consumers;
130
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
130
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
131
131
  const published = (id: string): boolean => {
132
132
  const symbol = byId.get(id);
133
133
  return (
@@ -1,5 +1,5 @@
1
1
  import { CODE } from "../contract/contract.generated.ts";
2
- import type { Config } from "../config.ts";
2
+ import type { ResolvedConfig } from "../config.ts";
3
3
  import type { Finding } from "../finding.ts";
4
4
  import type { Emitter, EmitterInput } from "./emitter.ts";
5
5
 
@@ -15,10 +15,10 @@ const FILE_START = 1;
15
15
  * completeness and listing no project declares it of a matrix the run derived, and a
16
16
  * derived matrix is never complete.
17
17
  */
18
- function completeMatrix(config: Config): boolean {
18
+ function completeMatrix(config: ResolvedConfig): boolean {
19
19
  return (
20
- config.analysis.matrixComplete &&
21
- config.analysis.configurations.some((entry) => entry.shape === "project")
20
+ config.analysis.matrix.complete &&
21
+ config.analysis.configurations.some((entry) => "project" in entry)
22
22
  );
23
23
  }
24
24
 
@@ -218,7 +218,7 @@ export const unusedDeclarations: Emitter = (input) => [
218
218
  */
219
219
  function unusedImports(input: EmitterInput): Finding[] {
220
220
  const generated =
221
- input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
221
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
222
222
  return input.intraFunction.imports.flatMap((binding) =>
223
223
  binding.used || generated?.has(binding.position.path) === true
224
224
  ? []
@@ -266,13 +266,13 @@ function unreachable(
266
266
  export function narrowings(input: EmitterInput): readonly Finding[] {
267
267
  const facts = factsOf(input.swept.matrix.union);
268
268
  const { declared, loaded } = input.boundary.consumers;
269
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
269
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
270
270
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
271
271
  const candidates = new Map(
272
272
  input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
273
273
  );
274
274
  const generated =
275
- input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
275
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
276
276
 
277
277
  const findings: Finding[] = [];
278
278
  facts.graph.symbols.forEach((symbol, at) => {
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.ts";
8
8
  import type { AnalysisInputs } from "./analysis.ts";
9
- import { fails, type Config } from "./config.ts";
9
+ import { fails, type ResolvedConfig } from "./config.ts";
10
10
  import type { EdgeEvaluation } from "./edges.ts";
11
11
  import type { CompletedFinding, Finding } from "./finding.ts";
12
12
  import { decidedFindings, type Withheld } from "./findings/emitters.ts";
@@ -109,7 +109,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
109
109
  * suppression is stale or a finding's severity reaches the configured failing severity;
110
110
  * clean otherwise.
111
111
  */
112
- export function verdictOf(result: PassResult, config: Config): number {
112
+ export function verdictOf(result: PassResult, config: ResolvedConfig): number {
113
113
  if (result.totals.pending > 0) {
114
114
  return EXIT_PENDING;
115
115
  }
@@ -117,7 +117,7 @@ export function verdictOf(result: PassResult, config: Config): number {
117
117
  return EXIT_FINDINGS;
118
118
  }
119
119
  const failing = result.findings.some((finding) =>
120
- fails(finding.severity, config.reporters.failOn),
120
+ fails(finding.severity, config.reporters.fail_on),
121
121
  );
122
122
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
123
123
  }
@@ -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,74 @@
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 { joinPath, relativePath } from "../paths.ts";
11
+ import { message } from "../product.ts";
12
+ import { utf8Bytes } from "../reporters/sha256.ts";
13
+ import { UsageError } from "../usage.ts";
14
+
15
+ /**
16
+ * A document at the target root that is not one to read: its name resolves outside the
17
+ * target root or to anything but a regular file, or the read fails. Nothing is read from
18
+ * it, and the run ends with the failure code.
19
+ */
20
+ export class TargetDocumentError extends Error {
21
+ constructor(label: string, reason: string) {
22
+ super(`${label}: ${reason}`);
23
+ this.name = "TargetDocumentError";
24
+ }
25
+ }
26
+
27
+ /** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
28
+ export function refuseOverBound(label: string, text: string): void {
29
+ if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
30
+ throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
31
+ }
32
+ }
33
+
34
+ /** What `read` returns, or its throw as the document's {@link TargetDocumentError}. */
35
+ function attempt<T>(label: string, read: () => T): T {
36
+ try {
37
+ return read();
38
+ } catch (error: unknown) {
39
+ throw new TargetDocumentError(label, message(error));
40
+ }
41
+ }
42
+
43
+ /**
44
+ * The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
45
+ * there, each refusal naming it `label`. A document past the bound is read one byte past
46
+ * it and refused as {@link refuseOverBound} refuses it.
47
+ */
48
+ export function readTargetDocument(
49
+ host: Host,
50
+ targetRoot: string,
51
+ name: string,
52
+ label = name,
53
+ ): string | undefined {
54
+ const path = joinPath(targetRoot, name);
55
+ const kind = attempt(label, () => host.kindOf(path));
56
+ if (kind === "absent") {
57
+ return undefined;
58
+ }
59
+ const real = attempt(label, () => host.realPath(path));
60
+ if (
61
+ relativePath(
62
+ attempt(label, () => host.realPath(targetRoot)),
63
+ real,
64
+ ) === undefined
65
+ ) {
66
+ throw new TargetDocumentError(label, `resolves to ${real}, outside the target root`);
67
+ }
68
+ if (kind !== "file") {
69
+ throw new TargetDocumentError(label, "is not a regular file");
70
+ }
71
+ const text = attempt(label, () => host.readFile(real, CONFIG_MAX_BYTES + 1));
72
+ refuseOverBound(label, text);
73
+ return text;
74
+ }
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
@@ -53,7 +53,7 @@ function isInjected(node: Node): boolean {
53
53
  /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
54
54
  function registrations<Brand>(input: DetectorInput<Brand>): readonly Registration[] {
55
55
  const { project, held } = input;
56
- const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
56
+ const resolved = resolveEntries(project, held, input.ts.injection_registrations);
57
57
  if (!resolved.some(namesADeclaration)) {
58
58
  return [];
59
59
  }