@cplieger/deadset-ts 3.3.1 → 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.
@@ -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))
@@ -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 };
@@ -1,5 +1,10 @@
1
1
  import { ConfigError } from "./config.js";
2
2
  const leaf = { kind: "leaf" };
3
+ /** A list of declarations, each entry in one of the three shapes the `ts` section states. */
4
+ const declarations = {
5
+ kind: "list",
6
+ members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
7
+ };
3
8
  /**
4
9
  * The closed key list the Contract's configuration schema declares, derived from
5
10
  * that schema key by key. A test pins the two equal, so a Contract change that
@@ -44,7 +49,19 @@ export const SCHEMA_ROOT = {
44
49
  },
45
50
  },
46
51
  go: { kind: "section", members: {} },
47
- ts: { kind: "section", members: { test_files: leaf, entry_files: leaf } },
52
+ ts: {
53
+ kind: "section",
54
+ members: {
55
+ test_files: leaf,
56
+ entry_files: leaf,
57
+ injection_registrations: declarations,
58
+ lifecycle_contracts: {
59
+ kind: "list",
60
+ members: { components: declarations, bases: declarations, members: leaf },
61
+ },
62
+ serializers: declarations,
63
+ },
64
+ },
48
65
  provenance: { kind: "map" },
49
66
  },
50
67
  };