@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.
package/src/calls.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The calls and decorators of one project that the configured classes read: what each
3
+ * callee resolves to, and which of the target's classes a call passes or a decorator
4
+ * is attached to. A class is passed as an argument, or as an element of an array
5
+ * literal or a property value of an object literal written as one, at any depth, and a
6
+ * decorator written as a call is a call.
7
+ */
8
+
9
+ import {
10
+ isArrayLiteralExpression,
11
+ isCallExpression,
12
+ isClassDeclaration,
13
+ isDecorator,
14
+ isIdentifier,
15
+ isObjectLiteralExpression,
16
+ isParenthesizedExpression,
17
+ isPropertyAccessExpression,
18
+ isPropertyAssignment,
19
+ isShorthandPropertyAssignment,
20
+ isSpreadElement,
21
+ type CallExpression,
22
+ type Decorator,
23
+ type Expression,
24
+ type Node,
25
+ } from "@typescript/native/unstable/ast";
26
+ import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
27
+ import type { AliasChains } from "./alias-chain.ts";
28
+ import { nodeKey, type Inventory } from "./inventory.ts";
29
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
30
+ import type { ProjectView } from "./session.ts";
31
+
32
+ /** One value written where a call passes it, and the node its declaration is resolved from. */
33
+ export interface PassedValue {
34
+ /** Where the value is written, which is where a record of it sits. */
35
+ readonly site: Node;
36
+ /** The name the value is resolved through, or the shorthand assignment that holds it. */
37
+ readonly name: Node;
38
+ readonly shorthand: boolean;
39
+ }
40
+
41
+ /** One class of the target a call passes or a decorator is attached to, and where. */
42
+ export interface ClassUse {
43
+ /** The inventory's identifier of the class. */
44
+ readonly id: string;
45
+ readonly site: Node;
46
+ }
47
+
48
+ /** The name node a callee or a decorator expression resolves through, where it has one. */
49
+ export function calleeName(expression: Expression): Node | undefined {
50
+ if (isParenthesizedExpression(expression)) {
51
+ return calleeName(expression.expression);
52
+ }
53
+ if (isPropertyAccessExpression(expression)) {
54
+ return expression.name;
55
+ }
56
+ return isIdentifier(expression) ? expression : undefined;
57
+ }
58
+
59
+ /** The callee one decorator names: its expression, or the callee of the call it writes. */
60
+ export function decoratorCallee(decorator: Decorator): Node | undefined {
61
+ const expression = decorator.expression;
62
+ return calleeName(isCallExpression(expression) ? expression.expression : expression);
63
+ }
64
+
65
+ /** How one decorator is spelled in a record: its expression, without a call's arguments. */
66
+ export function spelledDecorator(decorator: Decorator): string {
67
+ const expression = decorator.expression;
68
+ const named = isCallExpression(expression) ? expression.expression : expression;
69
+ return `@${named.getText().replace(/\s+/gu, "")}`;
70
+ }
71
+
72
+ /** The decorators written on one declaration. */
73
+ export function decoratorsOf(node: Node): readonly Decorator[] {
74
+ return ((node as { readonly modifiers?: readonly Node[] }).modifiers ?? []).filter(isDecorator);
75
+ }
76
+
77
+ /**
78
+ * What each of a batch of nodes resolves to, every alias followed to the declaration
79
+ * it stands for. A node the checker resolves to nothing, and an alias that resolves
80
+ * to nothing, are absent from the answer. Each alias is followed once.
81
+ */
82
+ export function resolvedTargets<Brand>(
83
+ project: ProjectView<Brand>,
84
+ nodes: readonly Node[],
85
+ ): ReadonlyMap<Node, TSSymbol> {
86
+ const followed = new Map<number, TSSymbol | undefined>();
87
+ const targets = new Map<Node, TSSymbol>();
88
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
89
+ const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
90
+ project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
91
+ const node = run[index];
92
+ if (symbol === undefined || node === undefined) {
93
+ return;
94
+ }
95
+ let target: TSSymbol | undefined = symbol;
96
+ if ((symbol.flags & SymbolFlags.Alias) !== 0) {
97
+ if (!followed.has(symbol.id)) {
98
+ const aliased = project.checker.getAliasedSymbol(symbol);
99
+ followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
100
+ }
101
+ target = followed.get(symbol.id);
102
+ }
103
+ if (target !== undefined) {
104
+ targets.set(node, target);
105
+ }
106
+ });
107
+ }
108
+ return targets;
109
+ }
110
+
111
+ /** The values one expression passes: itself, or what an array or object literal holds. */
112
+ function passedIn(expression: Expression, found: PassedValue[]): void {
113
+ if (isParenthesizedExpression(expression)) {
114
+ passedIn(expression.expression, found);
115
+ return;
116
+ }
117
+ if (isArrayLiteralExpression(expression)) {
118
+ for (const element of expression.elements) {
119
+ passedIn(isSpreadElement(element) ? element.expression : element, found);
120
+ }
121
+ return;
122
+ }
123
+ if (isObjectLiteralExpression(expression)) {
124
+ for (const property of expression.properties) {
125
+ if (isPropertyAssignment(property)) {
126
+ passedIn(property.initializer, found);
127
+ } else if (isShorthandPropertyAssignment(property)) {
128
+ found.push({ site: property.name, name: property, shorthand: true });
129
+ }
130
+ }
131
+ return;
132
+ }
133
+ const name = calleeName(expression);
134
+ if (name !== undefined) {
135
+ found.push({ site: expression, name, shorthand: false });
136
+ }
137
+ }
138
+
139
+ /** The values one argument passes, a spread argument's included. */
140
+ export function valuesPassed(argument: Expression): readonly PassedValue[] {
141
+ const found: PassedValue[] = [];
142
+ passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
143
+ return found;
144
+ }
145
+
146
+ /**
147
+ * The classes of the target each call passes, by the call. A value that names no
148
+ * class of the target, a class of another program included, passes nothing here.
149
+ */
150
+ export function classesPassed<Brand>(
151
+ project: ProjectView<Brand>,
152
+ held: Inventory,
153
+ chains: AliasChains,
154
+ calls: readonly CallExpression[],
155
+ ): ReadonlyMap<CallExpression, readonly ClassUse[]> {
156
+ const passed = new Map(
157
+ calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]),
158
+ );
159
+ const all = [...passed.values()].flat();
160
+ const targets = resolvedTargets(
161
+ project,
162
+ all.filter((value) => !value.shorthand).map((value) => value.name),
163
+ );
164
+ const classes = new Set(
165
+ held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id),
166
+ );
167
+ const classesOf = (value: PassedValue): readonly string[] => {
168
+ const symbol = value.shorthand
169
+ ? project.shorthandValueAt(project.handle(value.name))
170
+ : targets.get(value.name);
171
+ return symbol === undefined
172
+ ? []
173
+ : chains.declarationsOf(symbol).filter((id) => classes.has(id));
174
+ };
175
+ return new Map(
176
+ [...passed].map(([call, values]) => [
177
+ call,
178
+ values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
179
+ ]),
180
+ );
181
+ }
182
+
183
+ /** One class declaration of the target and the decorators written on it. */
184
+ export interface DecoratedClass {
185
+ readonly id: string;
186
+ readonly decorators: readonly Decorator[];
187
+ }
188
+
189
+ /** Every class declaration of the target that carries a decorator. */
190
+ export function decoratedClasses<Brand>(
191
+ project: ProjectView<Brand>,
192
+ held: Inventory,
193
+ ): readonly DecoratedClass[] {
194
+ const found: DecoratedClass[] = [];
195
+ for (const file of project.ownSourceFiles()) {
196
+ const visit = (node: Node): void => {
197
+ if (isClassDeclaration(node)) {
198
+ const id = held.declarations.get(nodeKey(file, node));
199
+ const decorators = decoratorsOf(node);
200
+ if (id !== undefined && decorators.length > 0) {
201
+ found.push({ id, decorators });
202
+ }
203
+ }
204
+ node.forEachChild(visit);
205
+ };
206
+ file.forEachChild(visit);
207
+ }
208
+ return found;
209
+ }
210
+
211
+ /** Every call expression of the project's own files, decorators written as calls included. */
212
+ export function callsOf<Brand>(project: ProjectView<Brand>): readonly CallExpression[] {
213
+ const found: CallExpression[] = [];
214
+ for (const file of project.ownSourceFiles()) {
215
+ const visit = (node: Node): void => {
216
+ if (isCallExpression(node)) {
217
+ found.push(node);
218
+ }
219
+ node.forEachChild(visit);
220
+ };
221
+ file.forEachChild(visit);
222
+ }
223
+ return found;
224
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The members a value of one of the target's classes carries, as the checker answers
3
+ * for the class's type, so a member a base class declares is a member of every class
4
+ * that extends it and a member a subclass overrides is the subclass's own.
5
+ */
6
+
7
+ import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
8
+ import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
9
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
10
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
11
+ import type { ProjectView } from "./session.ts";
12
+
13
+ /** One member of a class's value that the target declares. */
14
+ export interface ClassMember {
15
+ /** The inventory's identifier of the declaration. */
16
+ readonly id: string;
17
+ /** The declaring node, whose syntax says whether it is a property, an accessor or a method. */
18
+ readonly node: Node;
19
+ /** The member's symbol flags, as the checker reads it on this class. */
20
+ readonly flags: SymbolFlags;
21
+ }
22
+
23
+ /** Which side of a class a reading asks for: its instances, or the class itself. */
24
+ export type ClassSide = "instance" | "static";
25
+
26
+ /** The name node of each class declaration of the target, by the class's identifier. */
27
+ export function classNames<Brand>(
28
+ project: ProjectView<Brand>,
29
+ held: Inventory,
30
+ ): ReadonlyMap<string, Node> {
31
+ const names = new Map<string, Node>();
32
+ for (const file of project.ownSourceFiles()) {
33
+ const visit = (node: Node): void => {
34
+ if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
35
+ const id = held.declarations.get(nodeKey(file, node));
36
+ if (id !== undefined) {
37
+ names.set(id, node.name);
38
+ }
39
+ }
40
+ node.forEachChild(visit);
41
+ };
42
+ file.forEachChild(visit);
43
+ }
44
+ return names;
45
+ }
46
+
47
+ /** The types of a batch of nodes, asked in capped batches. */
48
+ export function typesAt<Brand>(
49
+ project: ProjectView<Brand>,
50
+ nodes: readonly Node[],
51
+ ): (Type | undefined)[] {
52
+ const types: (Type | undefined)[] = [];
53
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
54
+ types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
55
+ }
56
+ return types;
57
+ }
58
+
59
+ /**
60
+ * The type one side of each class is: the instance type at the class's name, or the
61
+ * type of the class's own symbol, which is its constructor's.
62
+ */
63
+ function sideTypes<Brand>(
64
+ project: ProjectView<Brand>,
65
+ nodes: readonly Node[],
66
+ side: ClassSide,
67
+ ): (Type | undefined)[] {
68
+ if (side === "instance") {
69
+ return typesAt(project, nodes);
70
+ }
71
+ const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
72
+ const present = symbols.filter((symbol): symbol is TSSymbol => symbol !== undefined);
73
+ const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
74
+ let next = 0;
75
+ return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
76
+ }
77
+
78
+ /**
79
+ * The members one side of each class carries that the target declares, by the class.
80
+ * A member declared in a file outside the project's own, and the `prototype` a class
81
+ * carries on its static side, are not declarations of the target and are left out.
82
+ */
83
+ export function classMembers<Brand>(
84
+ project: ProjectView<Brand>,
85
+ held: Inventory,
86
+ classIds: ReadonlySet<string>,
87
+ side: ClassSide,
88
+ ): ReadonlyMap<string, readonly ClassMember[]> {
89
+ const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
90
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
91
+ const types = sideTypes(
92
+ project,
93
+ named.map(([, node]) => node),
94
+ side,
95
+ );
96
+ const members = new Map<string, readonly ClassMember[]>();
97
+ named.forEach(([id], index) => {
98
+ const type = types[index];
99
+ if (type === undefined) {
100
+ return;
101
+ }
102
+ const found: ClassMember[] = [];
103
+ for (const property of project.checker.getPropertiesOfType(type)) {
104
+ for (const handle of property.declarations) {
105
+ if (!own.has(handle.path)) {
106
+ continue;
107
+ }
108
+ const node = project.declarationAt(handle)?.node;
109
+ const declared =
110
+ node === undefined
111
+ ? undefined
112
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
113
+ if (node !== undefined && declared !== undefined) {
114
+ found.push({ id: declared, node, flags: property.flags });
115
+ }
116
+ }
117
+ }
118
+ members.set(id, found);
119
+ });
120
+ return members;
121
+ }
122
+
123
+ /**
124
+ * The last component of one member's stable reference, `:static` included, which is
125
+ * how a configuration names a member; undefined for a declaration that is no member.
126
+ */
127
+ export function memberComponent(
128
+ symbol: InventorySymbol,
129
+ byId: ReadonlyMap<string, InventorySymbol>,
130
+ ): string | undefined {
131
+ const parent = byId.get(symbol.parent);
132
+ if (parent === undefined || !symbol.ref.startsWith(`${parent.ref}.`)) {
133
+ return undefined;
134
+ }
135
+ return symbol.ref.slice(parent.ref.length + 1);
136
+ }
package/src/config.ts CHANGED
@@ -97,10 +97,38 @@ export interface Reporters {
97
97
  readonly failOn: Severity;
98
98
  }
99
99
 
100
+ /**
101
+ * One declaration a configuration entry names, in exactly one of three shapes: a
102
+ * declaration of the analyzed program by its stable symbol reference, a declaration a
103
+ * module exports by the bare specifier an import writes for the module and the
104
+ * declaration's path from its exports, or a declaration of the global scope by its
105
+ * path from that scope. A path joins the name and each member with dots and spells a
106
+ * static member with `:static` after it.
107
+ */
108
+ export type DeclarationEntry =
109
+ | { readonly shape: "symbol"; readonly symbol: string }
110
+ | { readonly shape: "module"; readonly module: string; readonly name: string }
111
+ | { readonly shape: "global"; readonly global: string };
112
+
113
+ /**
114
+ * One framework's lifecycle contract: the declarations whose use makes a class one of
115
+ * the framework's components, the classes whose subclasses are its components, and the
116
+ * names of the members the framework calls on a component, each spelled as the last
117
+ * component of a member's stable symbol reference.
118
+ */
119
+ export interface LifecycleContract {
120
+ readonly components: readonly DeclarationEntry[];
121
+ readonly bases: readonly DeclarationEntry[];
122
+ readonly members: readonly string[];
123
+ }
124
+
100
125
  /** The section the TypeScript analyzer owns. */
101
126
  export interface TSSection {
102
127
  readonly testFiles: readonly string[];
103
128
  readonly entryFiles: readonly string[];
129
+ readonly injectionRegistrations: readonly DeclarationEntry[];
130
+ readonly lifecycleContracts: readonly LifecycleContract[];
131
+ readonly serializers: readonly DeclarationEntry[];
104
132
  }
105
133
 
106
134
  /**
@@ -159,6 +187,9 @@ export function defaultConfig(): Config {
159
187
  ts: {
160
188
  testFiles: ["**/*.test.{ts,tsx,mts,cts}"],
161
189
  entryFiles: [],
190
+ injectionRegistrations: [],
191
+ lifecycleContracts: [],
192
+ serializers: [],
162
193
  },
163
194
  };
164
195
  }