@cplieger/deadset-ts 3.3.0 → 3.4.0

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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +8 -1
  3. package/dist/src/calls.js +154 -0
  4. package/dist/src/class-members.js +92 -0
  5. package/dist/src/config.js +3 -0
  6. package/dist/src/configured-declarations.js +232 -0
  7. package/dist/src/decorator.js +3 -10
  8. package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
  9. package/dist/src/findings/emitter.js +1 -0
  10. package/dist/src/findings/emitters.js +19 -0
  11. package/dist/src/findings/interfaces.js +2 -0
  12. package/dist/src/findings/intra-function.js +2 -0
  13. package/dist/src/findings/non-code-artifacts.js +2 -0
  14. package/dist/src/findings/reads-and-writes.js +2 -0
  15. package/dist/src/findings/self-check.js +2 -0
  16. package/dist/src/findings/unused-declarations.js +2 -0
  17. package/dist/src/findings/visibility-narrowing.js +2 -0
  18. package/dist/src/framework-lifecycle.js +192 -0
  19. package/dist/src/injection-container.js +101 -0
  20. package/dist/src/print.js +18 -0
  21. package/dist/src/resolve.js +98 -0
  22. package/dist/src/run.js +60 -176
  23. package/dist/src/schema.js +18 -1
  24. package/dist/src/serialization-contract.js +315 -0
  25. package/dist/src/value-reach.js +120 -0
  26. package/dist/src/verbs/print-config.js +9 -0
  27. package/dist/src/verbs/print-projects.js +34 -0
  28. package/dist/src/verbs/print-retained.js +23 -0
  29. package/dist/src/verbs/print-roots.js +36 -0
  30. package/dist/src/verbs/verb.js +5 -0
  31. package/dist/src/verbs/version.js +7 -0
  32. package/dist/src/version.js +1 -1
  33. package/jsr.json +1 -1
  34. package/package.json +2 -2
  35. package/src/analysis.ts +8 -1
  36. package/src/calls.ts +224 -0
  37. package/src/class-members.ts +136 -0
  38. package/src/config.ts +31 -0
  39. package/src/configured-declarations.ts +308 -0
  40. package/src/decorator.ts +4 -16
  41. package/src/exempt.ts +5 -1
  42. package/src/findings/dependencies-and-module-machinery.ts +4 -0
  43. package/src/findings/emitter.ts +19 -0
  44. package/src/findings/emitters.ts +21 -0
  45. package/src/findings/interfaces.ts +4 -0
  46. package/src/findings/intra-function.ts +4 -0
  47. package/src/findings/non-code-artifacts.ts +4 -0
  48. package/src/findings/reads-and-writes.ts +4 -0
  49. package/src/findings/self-check.ts +4 -0
  50. package/src/findings/unused-declarations.ts +4 -0
  51. package/src/findings/visibility-narrowing.ts +4 -0
  52. package/src/framework-lifecycle.ts +280 -0
  53. package/src/injection-container.ts +141 -0
  54. package/src/print.ts +20 -1
  55. package/src/resolve.ts +155 -0
  56. package/src/run.ts +64 -200
  57. package/src/schema.ts +19 -1
  58. package/src/serialization-contract.ts +492 -0
  59. package/src/value-reach.ts +152 -0
  60. package/src/verbs/print-config.ts +10 -0
  61. package/src/verbs/print-projects.ts +35 -0
  62. package/src/verbs/print-retained.ts +24 -0
  63. package/src/verbs/print-roots.ts +38 -0
  64. package/src/verbs/verb.ts +38 -0
  65. package/src/verbs/version.ts +8 -0
  66. package/src/version.ts +1 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The framework-lifecycle class: a member a `ts.lifecycle_contracts` entry lists is held
3
+ * back on each class that is the entry's component, being a class a decorator or a call
4
+ * the entry's `components` name is attached to or passes, or one extending, directly or
5
+ * through a chain, a class its `bases` name. A component's members are its type's, so a
6
+ * member a base class declares answers for every subclass.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isClassExpression,
12
+ isHeritageClause,
13
+ SyntaxKind,
14
+ type CallExpression,
15
+ type Node,
16
+ } from "@typescript/native/unstable/ast";
17
+ import type { Type } from "@typescript/native/unstable/sync";
18
+ import { aliasChains, type AliasChains } from "./alias-chain.ts";
19
+ import { calleeName, callsOf, classesPassed, decoratedClasses, decoratorCallee } from "./calls.ts";
20
+ import { classMembers, memberComponent, typesAt, type ClassSide } from "./class-members.ts";
21
+ import type { DeclarationEntry, LifecycleContract } from "./config.ts";
22
+ import {
23
+ entriesAt,
24
+ names,
25
+ namesADeclaration,
26
+ resolveEntries,
27
+ spelledEntry,
28
+ type ResolvedEntry,
29
+ } from "./configured-declarations.ts";
30
+ import type { DetectorInput, Evidence } from "./exempt.ts";
31
+ import { nodeKey, type Inventory } from "./inventory.ts";
32
+ import { renderPosition } from "./position.ts";
33
+ import type { ProjectView } from "./session.ts";
34
+
35
+ /** One class that is a component of one contract, and what says it is. */
36
+ interface Component {
37
+ readonly contract: LifecycleContract;
38
+ readonly id: string;
39
+ readonly site: Node;
40
+ readonly detail: string;
41
+ }
42
+
43
+ /** One contract's declarations, resolved in the project. */
44
+ interface ResolvedContract {
45
+ readonly contract: LifecycleContract;
46
+ readonly components: readonly ResolvedEntry[];
47
+ readonly bases: readonly ResolvedEntry[];
48
+ }
49
+
50
+ /** Every contract's components and bases, resolved in one pass over the entries. */
51
+ function resolveContracts<Brand>(
52
+ project: ProjectView<Brand>,
53
+ held: Inventory,
54
+ contracts: readonly LifecycleContract[],
55
+ ): readonly ResolvedContract[] {
56
+ const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
57
+ ...contract.components,
58
+ ...contract.bases,
59
+ ]);
60
+ const resolved = resolveEntries(project, held, entries);
61
+ let next = 0;
62
+ const take = (count: number): readonly ResolvedEntry[] => {
63
+ const taken = resolved.slice(next, next + count).filter(namesADeclaration);
64
+ next += count;
65
+ return taken;
66
+ };
67
+ return contracts.map((contract) => ({
68
+ contract,
69
+ components: take(contract.components.length),
70
+ bases: take(contract.bases.length),
71
+ }));
72
+ }
73
+
74
+ /** The classes a decorator or a call the contracts' components name makes a component. */
75
+ function byComponents<Brand>(
76
+ project: ProjectView<Brand>,
77
+ held: Inventory,
78
+ chains: AliasChains,
79
+ contracts: readonly ResolvedContract[],
80
+ ): readonly Component[] {
81
+ const routed = contracts.filter((one) => one.components.length > 0);
82
+ if (routed.length === 0) {
83
+ return [];
84
+ }
85
+ const contractOf = new Map(
86
+ routed.flatMap((one) => one.components.map((entry) => [entry, one.contract] as const)),
87
+ );
88
+ const calls = callsOf(project);
89
+ const decorated = decoratedClasses(project, held);
90
+ const named = entriesAt(
91
+ project,
92
+ chains,
93
+ [
94
+ ...calls.flatMap((call) => calleeName(call.expression) ?? []),
95
+ ...decorated.flatMap((one) =>
96
+ one.decorators.flatMap((decorator) => decoratorCallee(decorator) ?? []),
97
+ ),
98
+ ],
99
+ [...contractOf.keys()],
100
+ );
101
+ const naming = (node: Node | undefined): { contract: LifecycleContract; detail: string }[] =>
102
+ (node === undefined ? [] : (named.get(node) ?? [])).flatMap((entry) => {
103
+ const contract = contractOf.get(entry);
104
+ return contract === undefined ? [] : [{ contract, detail: spelledEntry(entry.entry, held) }];
105
+ });
106
+
107
+ const found: Component[] = [];
108
+ for (const one of decorated) {
109
+ for (const decorator of one.decorators) {
110
+ for (const { contract, detail } of naming(decoratorCallee(decorator))) {
111
+ found.push({ contract, id: one.id, site: decorator, detail: `decorated by @${detail}` });
112
+ }
113
+ }
114
+ }
115
+ const matched = new Map<CallExpression, { contract: LifecycleContract; detail: string }[]>();
116
+ for (const call of calls) {
117
+ const routes = naming(calleeName(call.expression));
118
+ if (routes.length > 0) {
119
+ matched.set(call, routes);
120
+ }
121
+ }
122
+ for (const [call, uses] of classesPassed(project, held, chains, [...matched.keys()])) {
123
+ for (const route of matched.get(call) ?? []) {
124
+ for (const use of uses) {
125
+ found.push({
126
+ contract: route.contract,
127
+ id: use.id,
128
+ site: use.site,
129
+ detail: `registered by ${route.detail}`,
130
+ });
131
+ }
132
+ }
133
+ }
134
+ return found;
135
+ }
136
+
137
+ /** Each class declaration of the target, with its name and the class its `extends` names. */
138
+ interface Declared {
139
+ readonly id: string;
140
+ readonly name: Node;
141
+ readonly extended: Node;
142
+ }
143
+
144
+ /** The class declarations of the target that extend another class. */
145
+ function extendingClasses<Brand>(
146
+ project: ProjectView<Brand>,
147
+ held: Inventory,
148
+ ): readonly Declared[] {
149
+ const found: Declared[] = [];
150
+ for (const file of project.ownSourceFiles()) {
151
+ const visit = (node: Node): void => {
152
+ if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
153
+ const id = held.declarations.get(nodeKey(file, node));
154
+ const clause = (node.heritageClauses ?? []).find(
155
+ (one) => isHeritageClause(one) && one.token === SyntaxKind.ExtendsKeyword,
156
+ );
157
+ const extended =
158
+ clause !== undefined && isHeritageClause(clause) ? clause.types[0] : undefined;
159
+ if (id !== undefined && extended !== undefined) {
160
+ found.push({ id, name: node.name, extended });
161
+ }
162
+ }
163
+ node.forEachChild(visit);
164
+ };
165
+ file.forEachChild(visit);
166
+ }
167
+ return found;
168
+ }
169
+
170
+ /** The classes that extend, directly or through a chain, a class the contracts' bases name. */
171
+ function byBases<Brand>(
172
+ project: ProjectView<Brand>,
173
+ held: Inventory,
174
+ chains: AliasChains,
175
+ contracts: readonly ResolvedContract[],
176
+ ): readonly Component[] {
177
+ const based = contracts.filter((one) => one.bases.length > 0);
178
+ if (based.length === 0) {
179
+ return [];
180
+ }
181
+ const classes = extendingClasses(project, held);
182
+ if (classes.length === 0) {
183
+ return [];
184
+ }
185
+ const checker = project.checker;
186
+ const ancestry = new Map<number, readonly Type[]>();
187
+ const ancestorsOf = (type: Type, seen: Set<number>): readonly Type[] => {
188
+ const known = ancestry.get(type.id);
189
+ if (known !== undefined) {
190
+ return known;
191
+ }
192
+ const declared = type.isClassOrInterface()
193
+ ? type
194
+ : type.isTypeReference()
195
+ ? type.getTarget()
196
+ : undefined;
197
+ if (declared === undefined || !declared.isClassOrInterface() || seen.has(declared.id)) {
198
+ return [];
199
+ }
200
+ seen.add(declared.id);
201
+ const found = checker
202
+ .getBaseTypes(declared)
203
+ .flatMap((base) => [base, ...ancestorsOf(base, seen)]);
204
+ ancestry.set(type.id, found);
205
+ return found;
206
+ };
207
+ const types = typesAt(
208
+ project,
209
+ classes.map((one) => one.name),
210
+ );
211
+
212
+ const found: Component[] = [];
213
+ classes.forEach((one, index) => {
214
+ const type = types[index];
215
+ if (type === undefined) {
216
+ return;
217
+ }
218
+ const ancestors = ancestorsOf(type, new Set()).flatMap((base) => base.getSymbol() ?? []);
219
+ for (const contract of based) {
220
+ for (const entry of contract.bases) {
221
+ if (ancestors.some((symbol) => names(entry, symbol, chains))) {
222
+ found.push({
223
+ contract: contract.contract,
224
+ id: one.id,
225
+ site: one.extended,
226
+ detail: `extends ${spelledEntry(entry.entry, held)}`,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ });
232
+ return found;
233
+ }
234
+
235
+ /**
236
+ * The framework-lifecycle detector: each member a contract lists, on each class that
237
+ * is a component of that contract's framework, recorded where the class was found to
238
+ * be one.
239
+ */
240
+ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
241
+ const { project, held, targetRoot } = input;
242
+ if (input.ts.lifecycleContracts.length === 0) {
243
+ return [];
244
+ }
245
+ const chains = aliasChains(project, held);
246
+ const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
247
+ const components = [
248
+ ...byComponents(project, held, chains, contracts),
249
+ ...byBases(project, held, chains, contracts),
250
+ ];
251
+ if (components.length === 0) {
252
+ return [];
253
+ }
254
+ const ids = new Set(components.map((one) => one.id));
255
+ const sides: readonly ClassSide[] = input.ts.lifecycleContracts.some((contract) =>
256
+ contract.members.some((name) => name.endsWith(":static")),
257
+ )
258
+ ? ["instance", "static"]
259
+ : ["instance"];
260
+ const members = sides.map((side) => classMembers(project, held, ids, side));
261
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
262
+
263
+ const found: Evidence[] = [];
264
+ for (const component of components) {
265
+ const listed = new Set(component.contract.members);
266
+ const site = renderPosition(
267
+ component.site.getSourceFile(),
268
+ targetRoot,
269
+ component.site.getStart(),
270
+ );
271
+ for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
272
+ const symbol = byId.get(member.id);
273
+ const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
274
+ if (spelled !== undefined && listed.has(spelled)) {
275
+ found.push({ id: member.id, detail: component.detail, site });
276
+ }
277
+ }
278
+ }
279
+ return found;
280
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The injection-container class: a container constructs a class passed to a call
3
+ * `ts.injection_registrations` names, or one a parameter of whose constructor carries a
4
+ * decorator, and populates each of its properties and accessors that carries one, a base
5
+ * class's included. A constructor and its parameters are no declarations of the
6
+ * inventory, so the class stands for them and the registration referencing it keeps it.
7
+ */
8
+
9
+ import {
10
+ isClassDeclaration,
11
+ isClassExpression,
12
+ isConstructorDeclaration,
13
+ isGetAccessorDeclaration,
14
+ isPropertyDeclaration,
15
+ isSetAccessorDeclaration,
16
+ type CallExpression,
17
+ type Node,
18
+ } from "@typescript/native/unstable/ast";
19
+ import { aliasChains } from "./alias-chain.ts";
20
+ import { calleeName, callsOf, classesPassed, decoratorsOf, spelledDecorator } from "./calls.ts";
21
+ import { classMembers, type ClassMember } from "./class-members.ts";
22
+ import {
23
+ entriesAt,
24
+ namesADeclaration,
25
+ resolveEntries,
26
+ spelledEntry,
27
+ type ResolvedEntry,
28
+ } from "./configured-declarations.ts";
29
+ import type { DetectorInput, Evidence } from "./exempt.ts";
30
+ import { nodeKey } from "./inventory.ts";
31
+ import { renderPosition } from "./position.ts";
32
+
33
+ /** One class a container constructs, by the registration that says so. */
34
+ interface Registration {
35
+ readonly id: string;
36
+ readonly site: Node;
37
+ readonly detail: string;
38
+ }
39
+
40
+ /** Whether one member declaration is a property or an accessor carrying a decorator. */
41
+ function isInjected(node: Node): boolean {
42
+ return (
43
+ (isPropertyDeclaration(node) ||
44
+ isGetAccessorDeclaration(node) ||
45
+ isSetAccessorDeclaration(node)) &&
46
+ decoratorsOf(node).length > 0
47
+ );
48
+ }
49
+
50
+ /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
51
+ function registrations<Brand>(input: DetectorInput<Brand>): readonly Registration[] {
52
+ const { project, held } = input;
53
+ const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
54
+ if (!resolved.some(namesADeclaration)) {
55
+ return [];
56
+ }
57
+ const chains = aliasChains(project, held);
58
+ const calls = callsOf(project);
59
+ const named = entriesAt(
60
+ project,
61
+ chains,
62
+ calls.flatMap((call) => calleeName(call.expression) ?? []),
63
+ resolved,
64
+ );
65
+ const matched = new Map<CallExpression, ResolvedEntry>();
66
+ for (const call of calls) {
67
+ const name = calleeName(call.expression);
68
+ const [entry] = name === undefined ? [] : (named.get(name) ?? []);
69
+ if (entry !== undefined) {
70
+ matched.set(call, entry);
71
+ }
72
+ }
73
+ return [...classesPassed(project, held, chains, [...matched.keys()])].flatMap(([call, uses]) => {
74
+ const entry = matched.get(call);
75
+ const detail = entry === undefined ? "" : `registered by ${spelledEntry(entry.entry, held)}`;
76
+ return uses.map((use) => ({ id: use.id, site: use.site, detail }));
77
+ });
78
+ }
79
+
80
+ /** The classes of the target a parameter of whose constructor carries a decorator. */
81
+ function constructedForParameters<Brand>(input: DetectorInput<Brand>): ReadonlySet<string> {
82
+ const { project, held } = input;
83
+ const found = new Set<string>();
84
+ for (const file of project.ownSourceFiles()) {
85
+ const visit = (node: Node): void => {
86
+ if (isClassDeclaration(node) || isClassExpression(node)) {
87
+ const id = held.declarations.get(nodeKey(file, node));
88
+ const asks = node.members.some(
89
+ (member) =>
90
+ isConstructorDeclaration(member) &&
91
+ member.parameters.some((parameter) => decoratorsOf(parameter).length > 0),
92
+ );
93
+ if (id !== undefined && asks) {
94
+ found.add(id);
95
+ }
96
+ }
97
+ node.forEachChild(visit);
98
+ };
99
+ file.forEachChild(visit);
100
+ }
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * The injection-container detector: each decorated property or accessor of a class a
106
+ * container constructs, recorded at the registration that names the class, or, for a
107
+ * class whose constructor asks for its parameters, at the member's own decorator.
108
+ */
109
+ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
110
+ const { project, held, targetRoot } = input;
111
+ const registered = registrations(input);
112
+ const constructed = constructedForParameters(input);
113
+ const classes = new Set([...registered.map((one) => one.id), ...constructed]);
114
+ if (classes.size === 0) {
115
+ return [];
116
+ }
117
+ const members = classMembers(project, held, classes, "instance");
118
+ const injected = (id: string): readonly ClassMember[] =>
119
+ (members.get(id) ?? []).filter((member) => isInjected(member.node));
120
+ const at = (node: Node) => renderPosition(node.getSourceFile(), targetRoot, node.getStart());
121
+
122
+ const found: Evidence[] = [];
123
+ for (const registration of registered) {
124
+ for (const member of injected(registration.id)) {
125
+ found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
126
+ }
127
+ }
128
+ for (const id of constructed) {
129
+ for (const member of injected(id)) {
130
+ const [decorator] = decoratorsOf(member.node);
131
+ if (decorator !== undefined) {
132
+ found.push({
133
+ id: member.id,
134
+ detail: `injected by ${spelledDecorator(decorator)}`,
135
+ site: at(decorator),
136
+ });
137
+ }
138
+ }
139
+ }
140
+ return found;
141
+ }
package/src/print.ts CHANGED
@@ -1,4 +1,16 @@
1
- import { renderOrigin, type Config, type Provenance } from "./config.ts";
1
+ import { renderOrigin, type Config, type DeclarationEntry, type Provenance } from "./config.ts";
2
+
3
+ /** One declaration entry as a document writes it: the members of its shape alone. */
4
+ function declarationOf(entry: DeclarationEntry): Record<string, string> {
5
+ switch (entry.shape) {
6
+ case "symbol":
7
+ return { symbol: entry.symbol };
8
+ case "module":
9
+ return { module: entry.module, name: entry.name };
10
+ case "global":
11
+ return { global: entry.global };
12
+ }
13
+ }
2
14
 
3
15
  /**
4
16
  * The resolved configuration and its provenance as one JSON object, indented,
@@ -47,6 +59,13 @@ export function printConfig(config: Config, provenance: Provenance): string {
47
59
  ts: {
48
60
  test_files: config.ts.testFiles,
49
61
  entry_files: config.ts.entryFiles,
62
+ injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
63
+ lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
64
+ components: entry.components.map(declarationOf),
65
+ bases: entry.bases.map(declarationOf),
66
+ members: entry.members,
67
+ })),
68
+ serializers: config.ts.serializers.map(declarationOf),
50
69
  },
51
70
  provenance: Object.fromEntries(
52
71
  [...provenance]
package/src/resolve.ts CHANGED
@@ -6,10 +6,12 @@ import {
6
6
  type Config,
7
7
  type Confidence,
8
8
  type ConsumerTests,
9
+ type DeclarationEntry,
9
10
  type Format,
10
11
  type GeneratedFiles,
11
12
  type Inputs,
12
13
  type Language,
14
+ type LifecycleContract,
13
15
  type Origin,
14
16
  type Provenance,
15
17
  type Severity,
@@ -94,6 +96,9 @@ interface Doc {
94
96
  readonly failOn: Severity | undefined;
95
97
  readonly testFiles: readonly string[] | undefined;
96
98
  readonly entryFiles: readonly string[] | undefined;
99
+ readonly injectionRegistrations: readonly DeclarationEntry[] | undefined;
100
+ readonly lifecycleContracts: readonly LifecycleContract[] | undefined;
101
+ readonly serializers: readonly DeclarationEntry[] | undefined;
97
102
  }
98
103
 
99
104
  /** One document the resolution reads, with the origin its settings carry. */
@@ -476,6 +481,137 @@ function readExemptions(
476
481
  return disabled;
477
482
  }
478
483
 
484
+ /** The members that name a declaration, each belonging to exactly one shape. */
485
+ const SHAPE_MEMBERS = ["symbol", "module", "name", "global"] as const;
486
+
487
+ /** The prefix of a stable symbol reference in the TypeScript form. */
488
+ const SYMBOL_PREFIX = "ts://";
489
+
490
+ /**
491
+ * A bare specifier: neither relative nor absolute, and not one the package's own
492
+ * imports field maps, each of which names a file of the analyzed program.
493
+ */
494
+ const BARE_SPECIFIER = /^[^./#]/u;
495
+
496
+ /**
497
+ * One declaration entry, read as the one shape its members name. An entry carrying
498
+ * members of two shapes, or of none, is refused at the entry, as is a module named
499
+ * without the declaration's path or a path without its module.
500
+ */
501
+ function readDeclaration(entry: unknown, at: string, label: string): DeclarationEntry {
502
+ if (!isRecord(entry)) {
503
+ throw malformed(label, at, "is not an object");
504
+ }
505
+ const named = SHAPE_MEMBERS.filter((name) => Object.hasOwn(entry, name));
506
+ if (named.length === 1 && named[0] === "symbol") {
507
+ const symbol = requireMember(
508
+ readString(entry, "symbol", `${at}.symbol`, label),
509
+ `${at}.symbol`,
510
+ label,
511
+ );
512
+ if (!symbol.startsWith(SYMBOL_PREFIX)) {
513
+ throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
514
+ }
515
+ return { shape: "symbol", symbol };
516
+ }
517
+ if (named.length === 1 && named[0] === "global") {
518
+ const path = `${at}.global`;
519
+ return {
520
+ shape: "global",
521
+ global: requireMember(readString(entry, "global", path, label), path, label),
522
+ };
523
+ }
524
+ if (named.length === 2 && named.includes("module") && named.includes("name")) {
525
+ const module = requireMember(
526
+ readString(entry, "module", `${at}.module`, label),
527
+ `${at}.module`,
528
+ label,
529
+ );
530
+ if (!BARE_SPECIFIER.test(module)) {
531
+ throw malformed(
532
+ label,
533
+ `${at}.module`,
534
+ `${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`,
535
+ );
536
+ }
537
+ const path = `${at}.name`;
538
+ return {
539
+ shape: "module",
540
+ module,
541
+ name: requireMember(readString(entry, "name", path, label), path, label),
542
+ };
543
+ }
544
+ throw malformed(
545
+ label,
546
+ at,
547
+ named.length === 0
548
+ ? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
549
+ : `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`,
550
+ );
551
+ }
552
+
553
+ /** One array of declaration entries, absent where the document does not write it. */
554
+ function readDeclarations(
555
+ parent: Record<string, unknown> | undefined,
556
+ name: string,
557
+ path: string,
558
+ label: string,
559
+ ): readonly DeclarationEntry[] | undefined {
560
+ const value = member(parent, name);
561
+ if (value === undefined) {
562
+ return undefined;
563
+ }
564
+ if (!Array.isArray(value)) {
565
+ throw malformed(label, path, "is not an array");
566
+ }
567
+ return (value as unknown[]).map((entry, index) =>
568
+ readDeclaration(entry, `${path}[${String(index)}]`, label),
569
+ );
570
+ }
571
+
572
+ /**
573
+ * One lifecycle contract. The member names are required, and the entry names at least
574
+ * one component declaration or one base class, because a contract naming neither
575
+ * makes no class a component and so retains nothing.
576
+ */
577
+ function readLifecycleContract(entry: unknown, at: string, label: string): LifecycleContract {
578
+ if (!isRecord(entry)) {
579
+ throw malformed(label, at, "is not an object");
580
+ }
581
+ const components = readDeclarations(entry, "components", `${at}.components`, label) ?? [];
582
+ const bases = readDeclarations(entry, "bases", `${at}.bases`, label) ?? [];
583
+ const members = readStrings(entry, "members", `${at}.members`, label, 1);
584
+ if (members === undefined) {
585
+ throw malformed(label, `${at}.members`, "is required and names nothing");
586
+ }
587
+ if (components.length === 0 && bases.length === 0) {
588
+ throw malformed(
589
+ label,
590
+ at,
591
+ 'names no declaration in "components" and no class in "bases", so it makes no class a component',
592
+ );
593
+ }
594
+ return { components, bases, members };
595
+ }
596
+
597
+ /** The lifecycle contracts, one per framework the project declares. */
598
+ function readLifecycleContracts(
599
+ ts: Record<string, unknown> | undefined,
600
+ label: string,
601
+ ): readonly LifecycleContract[] | undefined {
602
+ const path = "ts.lifecycle_contracts";
603
+ const value = member(ts, "lifecycle_contracts");
604
+ if (value === undefined) {
605
+ return undefined;
606
+ }
607
+ if (!Array.isArray(value)) {
608
+ throw malformed(label, path, "is not an array");
609
+ }
610
+ return (value as unknown[]).map((entry, index) =>
611
+ readLifecycleContract(entry, `${path}[${String(index)}]`, label),
612
+ );
613
+ }
614
+
479
615
  /**
480
616
  * Reads one already-parsed configuration document as the settings it supplies,
481
617
  * refusing every constraint the closed key list declares that a parse cannot
@@ -545,6 +681,14 @@ function readDoc(value: unknown, label: string): Doc {
545
681
  failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
546
682
  testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
547
683
  entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
684
+ injectionRegistrations: readDeclarations(
685
+ ts,
686
+ "injection_registrations",
687
+ "ts.injection_registrations",
688
+ label,
689
+ ),
690
+ lifecycleContracts: readLifecycleContracts(ts, label),
691
+ serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
548
692
  };
549
693
  }
550
694
 
@@ -818,6 +962,17 @@ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Or
818
962
  ts: {
819
963
  testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
820
964
  entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
965
+ injectionRegistrations: at(
966
+ "ts.injection_registrations",
967
+ defaults.ts.injectionRegistrations,
968
+ (doc) => doc.injectionRegistrations,
969
+ ),
970
+ lifecycleContracts: at(
971
+ "ts.lifecycle_contracts",
972
+ defaults.ts.lifecycleContracts,
973
+ (doc) => doc.lifecycleContracts,
974
+ ),
975
+ serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
821
976
  },
822
977
  };
823
978