@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.
- package/README.md +1 -1
- package/dist/src/analysis.js +8 -1
- package/dist/src/calls.js +154 -0
- package/dist/src/class-members.js +92 -0
- package/dist/src/config.js +3 -0
- package/dist/src/configured-declarations.js +232 -0
- package/dist/src/decorator.js +3 -10
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
- package/dist/src/findings/emitter.js +1 -0
- package/dist/src/findings/emitters.js +19 -0
- package/dist/src/findings/interfaces.js +2 -0
- package/dist/src/findings/intra-function.js +2 -0
- package/dist/src/findings/non-code-artifacts.js +2 -0
- package/dist/src/findings/reads-and-writes.js +2 -0
- package/dist/src/findings/self-check.js +2 -0
- package/dist/src/findings/unused-declarations.js +2 -0
- package/dist/src/findings/visibility-narrowing.js +2 -0
- package/dist/src/framework-lifecycle.js +192 -0
- package/dist/src/injection-container.js +101 -0
- package/dist/src/print.js +18 -0
- package/dist/src/resolve.js +98 -0
- package/dist/src/run.js +60 -176
- package/dist/src/schema.js +18 -1
- package/dist/src/serialization-contract.js +315 -0
- package/dist/src/value-reach.js +120 -0
- package/dist/src/verbs/print-config.js +9 -0
- package/dist/src/verbs/print-projects.js +34 -0
- package/dist/src/verbs/print-retained.js +23 -0
- package/dist/src/verbs/print-roots.js +36 -0
- package/dist/src/verbs/verb.js +5 -0
- package/dist/src/verbs/version.js +7 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +8 -1
- package/src/calls.ts +224 -0
- package/src/class-members.ts +136 -0
- package/src/config.ts +31 -0
- package/src/configured-declarations.ts +308 -0
- package/src/decorator.ts +4 -16
- package/src/exempt.ts +5 -1
- package/src/findings/dependencies-and-module-machinery.ts +4 -0
- package/src/findings/emitter.ts +19 -0
- package/src/findings/emitters.ts +21 -0
- package/src/findings/interfaces.ts +4 -0
- package/src/findings/intra-function.ts +4 -0
- package/src/findings/non-code-artifacts.ts +4 -0
- package/src/findings/reads-and-writes.ts +4 -0
- package/src/findings/self-check.ts +4 -0
- package/src/findings/unused-declarations.ts +4 -0
- package/src/findings/visibility-narrowing.ts +4 -0
- package/src/framework-lifecycle.ts +280 -0
- package/src/injection-container.ts +141 -0
- package/src/print.ts +20 -1
- package/src/resolve.ts +155 -0
- package/src/run.ts +64 -200
- package/src/schema.ts +19 -1
- package/src/serialization-contract.ts +492 -0
- package/src/value-reach.ts +152 -0
- package/src/verbs/print-config.ts +10 -0
- package/src/verbs/print-projects.ts +35 -0
- package/src/verbs/print-retained.ts +24 -0
- package/src/verbs/print-roots.ts +38 -0
- package/src/verbs/verb.ts +38 -0
- package/src/verbs/version.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
import { isClassDeclaration, isClassExpression, isHeritageClause, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { aliasChains } from "./alias-chain.js";
|
|
10
|
+
import { calleeName, callsOf, classesPassed, decoratedClasses, decoratorCallee } from "./calls.js";
|
|
11
|
+
import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
12
|
+
import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
13
|
+
import { nodeKey } from "./inventory.js";
|
|
14
|
+
import { renderPosition } from "./position.js";
|
|
15
|
+
/** Every contract's components and bases, resolved in one pass over the entries. */
|
|
16
|
+
function resolveContracts(project, held, contracts) {
|
|
17
|
+
const entries = contracts.flatMap((contract) => [
|
|
18
|
+
...contract.components,
|
|
19
|
+
...contract.bases,
|
|
20
|
+
]);
|
|
21
|
+
const resolved = resolveEntries(project, held, entries);
|
|
22
|
+
let next = 0;
|
|
23
|
+
const take = (count) => {
|
|
24
|
+
const taken = resolved.slice(next, next + count).filter(namesADeclaration);
|
|
25
|
+
next += count;
|
|
26
|
+
return taken;
|
|
27
|
+
};
|
|
28
|
+
return contracts.map((contract) => ({
|
|
29
|
+
contract,
|
|
30
|
+
components: take(contract.components.length),
|
|
31
|
+
bases: take(contract.bases.length),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
/** The classes a decorator or a call the contracts' components name makes a component. */
|
|
35
|
+
function byComponents(project, held, chains, contracts) {
|
|
36
|
+
const routed = contracts.filter((one) => one.components.length > 0);
|
|
37
|
+
if (routed.length === 0) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const contractOf = new Map(routed.flatMap((one) => one.components.map((entry) => [entry, one.contract])));
|
|
41
|
+
const calls = callsOf(project);
|
|
42
|
+
const decorated = decoratedClasses(project, held);
|
|
43
|
+
const named = entriesAt(project, chains, [
|
|
44
|
+
...calls.flatMap((call) => calleeName(call.expression) ?? []),
|
|
45
|
+
...decorated.flatMap((one) => one.decorators.flatMap((decorator) => decoratorCallee(decorator) ?? [])),
|
|
46
|
+
], [...contractOf.keys()]);
|
|
47
|
+
const naming = (node) => (node === undefined ? [] : (named.get(node) ?? [])).flatMap((entry) => {
|
|
48
|
+
const contract = contractOf.get(entry);
|
|
49
|
+
return contract === undefined ? [] : [{ contract, detail: spelledEntry(entry.entry, held) }];
|
|
50
|
+
});
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const one of decorated) {
|
|
53
|
+
for (const decorator of one.decorators) {
|
|
54
|
+
for (const { contract, detail } of naming(decoratorCallee(decorator))) {
|
|
55
|
+
found.push({ contract, id: one.id, site: decorator, detail: `decorated by @${detail}` });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const matched = new Map();
|
|
60
|
+
for (const call of calls) {
|
|
61
|
+
const routes = naming(calleeName(call.expression));
|
|
62
|
+
if (routes.length > 0) {
|
|
63
|
+
matched.set(call, routes);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [call, uses] of classesPassed(project, held, chains, [...matched.keys()])) {
|
|
67
|
+
for (const route of matched.get(call) ?? []) {
|
|
68
|
+
for (const use of uses) {
|
|
69
|
+
found.push({
|
|
70
|
+
contract: route.contract,
|
|
71
|
+
id: use.id,
|
|
72
|
+
site: use.site,
|
|
73
|
+
detail: `registered by ${route.detail}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
/** The class declarations of the target that extend another class. */
|
|
81
|
+
function extendingClasses(project, held) {
|
|
82
|
+
const found = [];
|
|
83
|
+
for (const file of project.ownSourceFiles()) {
|
|
84
|
+
const visit = (node) => {
|
|
85
|
+
if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
|
|
86
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
87
|
+
const clause = (node.heritageClauses ?? []).find((one) => isHeritageClause(one) && one.token === SyntaxKind.ExtendsKeyword);
|
|
88
|
+
const extended = clause !== undefined && isHeritageClause(clause) ? clause.types[0] : undefined;
|
|
89
|
+
if (id !== undefined && extended !== undefined) {
|
|
90
|
+
found.push({ id, name: node.name, extended });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
node.forEachChild(visit);
|
|
94
|
+
};
|
|
95
|
+
file.forEachChild(visit);
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
/** The classes that extend, directly or through a chain, a class the contracts' bases name. */
|
|
100
|
+
function byBases(project, held, chains, contracts) {
|
|
101
|
+
const based = contracts.filter((one) => one.bases.length > 0);
|
|
102
|
+
if (based.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const classes = extendingClasses(project, held);
|
|
106
|
+
if (classes.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const checker = project.checker;
|
|
110
|
+
const ancestry = new Map();
|
|
111
|
+
const ancestorsOf = (type, seen) => {
|
|
112
|
+
const known = ancestry.get(type.id);
|
|
113
|
+
if (known !== undefined) {
|
|
114
|
+
return known;
|
|
115
|
+
}
|
|
116
|
+
const declared = type.isClassOrInterface()
|
|
117
|
+
? type
|
|
118
|
+
: type.isTypeReference()
|
|
119
|
+
? type.getTarget()
|
|
120
|
+
: undefined;
|
|
121
|
+
if (declared === undefined || !declared.isClassOrInterface() || seen.has(declared.id)) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
seen.add(declared.id);
|
|
125
|
+
const found = checker
|
|
126
|
+
.getBaseTypes(declared)
|
|
127
|
+
.flatMap((base) => [base, ...ancestorsOf(base, seen)]);
|
|
128
|
+
ancestry.set(type.id, found);
|
|
129
|
+
return found;
|
|
130
|
+
};
|
|
131
|
+
const types = typesAt(project, classes.map((one) => one.name));
|
|
132
|
+
const found = [];
|
|
133
|
+
classes.forEach((one, index) => {
|
|
134
|
+
const type = types[index];
|
|
135
|
+
if (type === undefined) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const ancestors = ancestorsOf(type, new Set()).flatMap((base) => base.getSymbol() ?? []);
|
|
139
|
+
for (const contract of based) {
|
|
140
|
+
for (const entry of contract.bases) {
|
|
141
|
+
if (ancestors.some((symbol) => names(entry, symbol, chains))) {
|
|
142
|
+
found.push({
|
|
143
|
+
contract: contract.contract,
|
|
144
|
+
id: one.id,
|
|
145
|
+
site: one.extended,
|
|
146
|
+
detail: `extends ${spelledEntry(entry.entry, held)}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The framework-lifecycle detector: each member a contract lists, on each class that
|
|
156
|
+
* is a component of that contract's framework, recorded where the class was found to
|
|
157
|
+
* be one.
|
|
158
|
+
*/
|
|
159
|
+
export function frameworkLifecycle(input) {
|
|
160
|
+
const { project, held, targetRoot } = input;
|
|
161
|
+
if (input.ts.lifecycleContracts.length === 0) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const chains = aliasChains(project, held);
|
|
165
|
+
const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
|
|
166
|
+
const components = [
|
|
167
|
+
...byComponents(project, held, chains, contracts),
|
|
168
|
+
...byBases(project, held, chains, contracts),
|
|
169
|
+
];
|
|
170
|
+
if (components.length === 0) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
const ids = new Set(components.map((one) => one.id));
|
|
174
|
+
const sides = input.ts.lifecycleContracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
|
|
175
|
+
? ["instance", "static"]
|
|
176
|
+
: ["instance"];
|
|
177
|
+
const members = sides.map((side) => classMembers(project, held, ids, side));
|
|
178
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
179
|
+
const found = [];
|
|
180
|
+
for (const component of components) {
|
|
181
|
+
const listed = new Set(component.contract.members);
|
|
182
|
+
const site = renderPosition(component.site.getSourceFile(), targetRoot, component.site.getStart());
|
|
183
|
+
for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
|
|
184
|
+
const symbol = byId.get(member.id);
|
|
185
|
+
const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
|
|
186
|
+
if (spelled !== undefined && listed.has(spelled)) {
|
|
187
|
+
found.push({ id: member.id, detail: component.detail, site });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
import { isClassDeclaration, isClassExpression, isConstructorDeclaration, isGetAccessorDeclaration, isPropertyDeclaration, isSetAccessorDeclaration, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { aliasChains } from "./alias-chain.js";
|
|
10
|
+
import { calleeName, callsOf, classesPassed, decoratorsOf, spelledDecorator } from "./calls.js";
|
|
11
|
+
import { classMembers } from "./class-members.js";
|
|
12
|
+
import { entriesAt, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
13
|
+
import { nodeKey } from "./inventory.js";
|
|
14
|
+
import { renderPosition } from "./position.js";
|
|
15
|
+
/** Whether one member declaration is a property or an accessor carrying a decorator. */
|
|
16
|
+
function isInjected(node) {
|
|
17
|
+
return ((isPropertyDeclaration(node) ||
|
|
18
|
+
isGetAccessorDeclaration(node) ||
|
|
19
|
+
isSetAccessorDeclaration(node)) &&
|
|
20
|
+
decoratorsOf(node).length > 0);
|
|
21
|
+
}
|
|
22
|
+
/** The classes of the target a call whose callee an entry names passes, with that call's entry. */
|
|
23
|
+
function registrations(input) {
|
|
24
|
+
const { project, held } = input;
|
|
25
|
+
const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
|
|
26
|
+
if (!resolved.some(namesADeclaration)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const chains = aliasChains(project, held);
|
|
30
|
+
const calls = callsOf(project);
|
|
31
|
+
const named = entriesAt(project, chains, calls.flatMap((call) => calleeName(call.expression) ?? []), resolved);
|
|
32
|
+
const matched = new Map();
|
|
33
|
+
for (const call of calls) {
|
|
34
|
+
const name = calleeName(call.expression);
|
|
35
|
+
const [entry] = name === undefined ? [] : (named.get(name) ?? []);
|
|
36
|
+
if (entry !== undefined) {
|
|
37
|
+
matched.set(call, entry);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...classesPassed(project, held, chains, [...matched.keys()])].flatMap(([call, uses]) => {
|
|
41
|
+
const entry = matched.get(call);
|
|
42
|
+
const detail = entry === undefined ? "" : `registered by ${spelledEntry(entry.entry, held)}`;
|
|
43
|
+
return uses.map((use) => ({ id: use.id, site: use.site, detail }));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** The classes of the target a parameter of whose constructor carries a decorator. */
|
|
47
|
+
function constructedForParameters(input) {
|
|
48
|
+
const { project, held } = input;
|
|
49
|
+
const found = new Set();
|
|
50
|
+
for (const file of project.ownSourceFiles()) {
|
|
51
|
+
const visit = (node) => {
|
|
52
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
53
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
54
|
+
const asks = node.members.some((member) => isConstructorDeclaration(member) &&
|
|
55
|
+
member.parameters.some((parameter) => decoratorsOf(parameter).length > 0));
|
|
56
|
+
if (id !== undefined && asks) {
|
|
57
|
+
found.add(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
node.forEachChild(visit);
|
|
61
|
+
};
|
|
62
|
+
file.forEachChild(visit);
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The injection-container detector: each decorated property or accessor of a class a
|
|
68
|
+
* container constructs, recorded at the registration that names the class, or, for a
|
|
69
|
+
* class whose constructor asks for its parameters, at the member's own decorator.
|
|
70
|
+
*/
|
|
71
|
+
export function injectionContainer(input) {
|
|
72
|
+
const { project, held, targetRoot } = input;
|
|
73
|
+
const registered = registrations(input);
|
|
74
|
+
const constructed = constructedForParameters(input);
|
|
75
|
+
const classes = new Set([...registered.map((one) => one.id), ...constructed]);
|
|
76
|
+
if (classes.size === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const members = classMembers(project, held, classes, "instance");
|
|
80
|
+
const injected = (id) => (members.get(id) ?? []).filter((member) => isInjected(member.node));
|
|
81
|
+
const at = (node) => renderPosition(node.getSourceFile(), targetRoot, node.getStart());
|
|
82
|
+
const found = [];
|
|
83
|
+
for (const registration of registered) {
|
|
84
|
+
for (const member of injected(registration.id)) {
|
|
85
|
+
found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const id of constructed) {
|
|
89
|
+
for (const member of injected(id)) {
|
|
90
|
+
const [decorator] = decoratorsOf(member.node);
|
|
91
|
+
if (decorator !== undefined) {
|
|
92
|
+
found.push({
|
|
93
|
+
id: member.id,
|
|
94
|
+
detail: `injected by ${spelledDecorator(decorator)}`,
|
|
95
|
+
site: at(decorator),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
package/dist/src/print.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { renderOrigin } from "./config.js";
|
|
2
|
+
/** One declaration entry as a document writes it: the members of its shape alone. */
|
|
3
|
+
function declarationOf(entry) {
|
|
4
|
+
switch (entry.shape) {
|
|
5
|
+
case "symbol":
|
|
6
|
+
return { symbol: entry.symbol };
|
|
7
|
+
case "module":
|
|
8
|
+
return { module: entry.module, name: entry.name };
|
|
9
|
+
case "global":
|
|
10
|
+
return { global: entry.global };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* The resolved configuration and its provenance as one JSON object, indented,
|
|
4
15
|
* ending in a newline.
|
|
@@ -44,6 +55,13 @@ export function printConfig(config, provenance) {
|
|
|
44
55
|
ts: {
|
|
45
56
|
test_files: config.ts.testFiles,
|
|
46
57
|
entry_files: config.ts.entryFiles,
|
|
58
|
+
injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
|
|
59
|
+
lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
|
|
60
|
+
components: entry.components.map(declarationOf),
|
|
61
|
+
bases: entry.bases.map(declarationOf),
|
|
62
|
+
members: entry.members,
|
|
63
|
+
})),
|
|
64
|
+
serializers: config.ts.serializers.map(declarationOf),
|
|
47
65
|
},
|
|
48
66
|
provenance: Object.fromEntries([...provenance]
|
|
49
67
|
.sort(([a], [b]) => (a < b ? -1 : 1))
|
package/dist/src/resolve.js
CHANGED
|
@@ -292,6 +292,98 @@ function readExemptions(document, label) {
|
|
|
292
292
|
}
|
|
293
293
|
return disabled;
|
|
294
294
|
}
|
|
295
|
+
/** The members that name a declaration, each belonging to exactly one shape. */
|
|
296
|
+
const SHAPE_MEMBERS = ["symbol", "module", "name", "global"];
|
|
297
|
+
/** The prefix of a stable symbol reference in the TypeScript form. */
|
|
298
|
+
const SYMBOL_PREFIX = "ts://";
|
|
299
|
+
/**
|
|
300
|
+
* A bare specifier: neither relative nor absolute, and not one the package's own
|
|
301
|
+
* imports field maps, each of which names a file of the analyzed program.
|
|
302
|
+
*/
|
|
303
|
+
const BARE_SPECIFIER = /^[^./#]/u;
|
|
304
|
+
/**
|
|
305
|
+
* One declaration entry, read as the one shape its members name. An entry carrying
|
|
306
|
+
* members of two shapes, or of none, is refused at the entry, as is a module named
|
|
307
|
+
* without the declaration's path or a path without its module.
|
|
308
|
+
*/
|
|
309
|
+
function readDeclaration(entry, at, label) {
|
|
310
|
+
if (!isRecord(entry)) {
|
|
311
|
+
throw malformed(label, at, "is not an object");
|
|
312
|
+
}
|
|
313
|
+
const named = SHAPE_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
314
|
+
if (named.length === 1 && named[0] === "symbol") {
|
|
315
|
+
const symbol = requireMember(readString(entry, "symbol", `${at}.symbol`, label), `${at}.symbol`, label);
|
|
316
|
+
if (!symbol.startsWith(SYMBOL_PREFIX)) {
|
|
317
|
+
throw malformed(label, `${at}.symbol`, `${JSON.stringify(symbol)} is not a ts:// reference`);
|
|
318
|
+
}
|
|
319
|
+
return { shape: "symbol", symbol };
|
|
320
|
+
}
|
|
321
|
+
if (named.length === 1 && named[0] === "global") {
|
|
322
|
+
const path = `${at}.global`;
|
|
323
|
+
return {
|
|
324
|
+
shape: "global",
|
|
325
|
+
global: requireMember(readString(entry, "global", path, label), path, label),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (named.length === 2 && named.includes("module") && named.includes("name")) {
|
|
329
|
+
const module = requireMember(readString(entry, "module", `${at}.module`, label), `${at}.module`, label);
|
|
330
|
+
if (!BARE_SPECIFIER.test(module)) {
|
|
331
|
+
throw malformed(label, `${at}.module`, `${JSON.stringify(module)} is not a bare specifier: a file of the analyzed program is named by its symbol`);
|
|
332
|
+
}
|
|
333
|
+
const path = `${at}.name`;
|
|
334
|
+
return {
|
|
335
|
+
shape: "module",
|
|
336
|
+
module,
|
|
337
|
+
name: requireMember(readString(entry, "name", path, label), path, label),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
throw malformed(label, at, named.length === 0
|
|
341
|
+
? 'names no declaration: an entry carries "symbol", "module" with "name", or "global"'
|
|
342
|
+
: `carries ${spell(named)}, which is not one of "symbol", "module" with "name", or "global"`);
|
|
343
|
+
}
|
|
344
|
+
/** One array of declaration entries, absent where the document does not write it. */
|
|
345
|
+
function readDeclarations(parent, name, path, label) {
|
|
346
|
+
const value = member(parent, name);
|
|
347
|
+
if (value === undefined) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
if (!Array.isArray(value)) {
|
|
351
|
+
throw malformed(label, path, "is not an array");
|
|
352
|
+
}
|
|
353
|
+
return value.map((entry, index) => readDeclaration(entry, `${path}[${String(index)}]`, label));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* One lifecycle contract. The member names are required, and the entry names at least
|
|
357
|
+
* one component declaration or one base class, because a contract naming neither
|
|
358
|
+
* makes no class a component and so retains nothing.
|
|
359
|
+
*/
|
|
360
|
+
function readLifecycleContract(entry, at, label) {
|
|
361
|
+
if (!isRecord(entry)) {
|
|
362
|
+
throw malformed(label, at, "is not an object");
|
|
363
|
+
}
|
|
364
|
+
const components = readDeclarations(entry, "components", `${at}.components`, label) ?? [];
|
|
365
|
+
const bases = readDeclarations(entry, "bases", `${at}.bases`, label) ?? [];
|
|
366
|
+
const members = readStrings(entry, "members", `${at}.members`, label, 1);
|
|
367
|
+
if (members === undefined) {
|
|
368
|
+
throw malformed(label, `${at}.members`, "is required and names nothing");
|
|
369
|
+
}
|
|
370
|
+
if (components.length === 0 && bases.length === 0) {
|
|
371
|
+
throw malformed(label, at, 'names no declaration in "components" and no class in "bases", so it makes no class a component');
|
|
372
|
+
}
|
|
373
|
+
return { components, bases, members };
|
|
374
|
+
}
|
|
375
|
+
/** The lifecycle contracts, one per framework the project declares. */
|
|
376
|
+
function readLifecycleContracts(ts, label) {
|
|
377
|
+
const path = "ts.lifecycle_contracts";
|
|
378
|
+
const value = member(ts, "lifecycle_contracts");
|
|
379
|
+
if (value === undefined) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
if (!Array.isArray(value)) {
|
|
383
|
+
throw malformed(label, path, "is not an array");
|
|
384
|
+
}
|
|
385
|
+
return value.map((entry, index) => readLifecycleContract(entry, `${path}[${String(index)}]`, label));
|
|
386
|
+
}
|
|
295
387
|
/**
|
|
296
388
|
* Reads one already-parsed configuration document as the settings it supplies,
|
|
297
389
|
* refusing every constraint the closed key list declares that a parse cannot
|
|
@@ -337,6 +429,9 @@ function readDoc(value, label) {
|
|
|
337
429
|
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
338
430
|
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
339
431
|
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
432
|
+
injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
|
|
433
|
+
lifecycleContracts: readLifecycleContracts(ts, label),
|
|
434
|
+
serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
|
|
340
435
|
};
|
|
341
436
|
}
|
|
342
437
|
/** Parses one document's text and walks it against the closed key list. */
|
|
@@ -538,6 +633,9 @@ function settleAll(inputs) {
|
|
|
538
633
|
ts: {
|
|
539
634
|
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
540
635
|
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
|
636
|
+
injectionRegistrations: at("ts.injection_registrations", defaults.ts.injectionRegistrations, (doc) => doc.injectionRegistrations),
|
|
637
|
+
lifecycleContracts: at("ts.lifecycle_contracts", defaults.ts.lifecycleContracts, (doc) => doc.lifecycleContracts),
|
|
638
|
+
serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
|
|
541
639
|
},
|
|
542
640
|
};
|
|
543
641
|
return { config, provenance };
|