@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/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/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/schema.js +18 -1
- package/dist/src/serialization-contract.js +315 -0
- package/dist/src/value-reach.js +120 -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/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/schema.ts +19 -1
- package/src/serialization-contract.ts +492 -0
- package/src/value-reach.ts +152 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serialization-contract class: the properties of a class whose values reach
|
|
3
|
+
* `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
|
|
4
|
+
* `toString` where a value is passed to an `unknown` or `any` parameter of a function
|
|
5
|
+
* outside the analysis. A function of the project handing such a parameter of its own
|
|
6
|
+
* to a destination is one, holding back what that destination does, to a fixpoint.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrowFunction, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { aliasChains } from "./alias-chain.js";
|
|
11
|
+
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
12
|
+
import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
13
|
+
import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
14
|
+
import { nodeKey } from "./inventory.js";
|
|
15
|
+
import { renderPosition } from "./position.js";
|
|
16
|
+
import { valueReach } from "./value-reach.js";
|
|
17
|
+
/** The serializer every run reads, whatever the configuration names. */
|
|
18
|
+
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
19
|
+
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
20
|
+
const CONVERSIONS = new Set(["toJSON", "toString"]);
|
|
21
|
+
/** The parameter types that keep nothing of the value they are given. */
|
|
22
|
+
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
23
|
+
/** The wider of two reaches, where either may be absent. */
|
|
24
|
+
function wider(a, b) {
|
|
25
|
+
return a === "conversions" || b === "conversions" ? "conversions" : (a ?? b);
|
|
26
|
+
}
|
|
27
|
+
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
28
|
+
function isStatic(node) {
|
|
29
|
+
const flags = node.modifierFlags ?? 0;
|
|
30
|
+
return (flags & ModifierFlags.Static) !== 0;
|
|
31
|
+
}
|
|
32
|
+
/** Whether one node opens a body whose `this` is not an instance of the enclosing class. */
|
|
33
|
+
function resetsThis(node) {
|
|
34
|
+
return (isFunctionDeclaration(node) ||
|
|
35
|
+
isFunctionExpression(node) ||
|
|
36
|
+
isClassStaticBlockDeclaration(node) ||
|
|
37
|
+
isStatic(node) ||
|
|
38
|
+
((isMethodDeclaration(node) ||
|
|
39
|
+
isGetAccessorDeclaration(node) ||
|
|
40
|
+
isSetAccessorDeclaration(node)) &&
|
|
41
|
+
isObjectLiteralExpression(node.parent)));
|
|
42
|
+
}
|
|
43
|
+
/** The parameters and declaring node of one function-like declaration of the project. */
|
|
44
|
+
function functionOf(node) {
|
|
45
|
+
if (isFunctionDeclaration(node) ||
|
|
46
|
+
(isMethodDeclaration(node) && !isObjectLiteralExpression(node.parent))) {
|
|
47
|
+
return { declared: node, parameters: node.parameters };
|
|
48
|
+
}
|
|
49
|
+
if (isVariableDeclaration(node) &&
|
|
50
|
+
node.initializer !== undefined &&
|
|
51
|
+
(isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))) {
|
|
52
|
+
return { declared: node, parameters: node.initializer.parameters };
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Whether one parameter is a `this` parameter, which types `this` and binds no argument. */
|
|
57
|
+
function isThisParameter(parameter) {
|
|
58
|
+
return isIdentifier(parameter.name) && parameter.name.text === "this";
|
|
59
|
+
}
|
|
60
|
+
/** Whether an argument is written in a form that holds no class instance, which saves a type. */
|
|
61
|
+
function holdsNoInstance(argument) {
|
|
62
|
+
return (isArrowFunction(argument) ||
|
|
63
|
+
isFunctionExpression(argument) ||
|
|
64
|
+
isStringLiteral(argument) ||
|
|
65
|
+
isNumericLiteral(argument) ||
|
|
66
|
+
isNoSubstitutionTemplateLiteral(argument) ||
|
|
67
|
+
argument.kind === SyntaxKind.TemplateExpression);
|
|
68
|
+
}
|
|
69
|
+
/** The expression an argument evaluates, through parentheses and a spread. */
|
|
70
|
+
function evaluated(argument) {
|
|
71
|
+
const value = isSpreadElement(argument) ? argument.expression : argument;
|
|
72
|
+
return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Every call of the project's own files with the class `this` stands for there, and
|
|
76
|
+
* every identifier-named parameter of every function, method and function-valued
|
|
77
|
+
* variable the project declares, at the position of the argument it binds.
|
|
78
|
+
*/
|
|
79
|
+
function walkProject(project, held) {
|
|
80
|
+
const sites = [];
|
|
81
|
+
const parameters = [];
|
|
82
|
+
for (const file of project.ownSourceFiles()) {
|
|
83
|
+
const visit = (node, self) => {
|
|
84
|
+
let inner = self;
|
|
85
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
86
|
+
inner = held.declarations.get(nodeKey(file, node));
|
|
87
|
+
}
|
|
88
|
+
else if (resetsThis(node)) {
|
|
89
|
+
inner = undefined;
|
|
90
|
+
}
|
|
91
|
+
if (node.kind === SyntaxKind.CallExpression) {
|
|
92
|
+
sites.push({ call: node, self });
|
|
93
|
+
}
|
|
94
|
+
const fn = functionOf(node);
|
|
95
|
+
const id = fn === undefined ? undefined : held.declarations.get(nodeKey(file, fn.declared));
|
|
96
|
+
if (fn !== undefined && id !== undefined) {
|
|
97
|
+
fn.parameters
|
|
98
|
+
.filter((parameter) => !isThisParameter(parameter))
|
|
99
|
+
.forEach((parameter, index) => {
|
|
100
|
+
if (isIdentifier(parameter.name)) {
|
|
101
|
+
parameters.push({
|
|
102
|
+
fn: id,
|
|
103
|
+
index,
|
|
104
|
+
rest: parameter.dotDotDotToken !== undefined,
|
|
105
|
+
name: parameter.name,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
node.forEachChild((child) => {
|
|
111
|
+
visit(child, inner);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
file.forEachChild((child) => {
|
|
115
|
+
visit(child, undefined);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { sites, parameters };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The parameters that keep nothing of the value they are given: one typed `unknown` or
|
|
122
|
+
* `any`, and a rest parameter whose elements are, each by its symbol.
|
|
123
|
+
*/
|
|
124
|
+
function erasedParameters(project, parameters) {
|
|
125
|
+
const types = typesAt(project, parameters.map((parameter) => parameter.name));
|
|
126
|
+
const erased = parameters.filter((parameter, index) => {
|
|
127
|
+
const type = types[index];
|
|
128
|
+
if (type === undefined) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (!parameter.rest) {
|
|
132
|
+
return (type.flags & ERASED) !== 0;
|
|
133
|
+
}
|
|
134
|
+
const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
|
|
135
|
+
return element !== undefined && (element.flags & ERASED) !== 0;
|
|
136
|
+
});
|
|
137
|
+
const bySymbol = new Map();
|
|
138
|
+
project
|
|
139
|
+
.symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
|
|
140
|
+
.forEach((symbol, index) => {
|
|
141
|
+
const parameter = erased[index];
|
|
142
|
+
if (symbol !== undefined && parameter !== undefined) {
|
|
143
|
+
bySymbol.set(symbol.id, parameter);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return bySymbol;
|
|
147
|
+
}
|
|
148
|
+
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
149
|
+
function handedOn(project, sites, erased) {
|
|
150
|
+
const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
|
|
151
|
+
const candidates = sites.flatMap((site) => site.call.arguments.flatMap((argument, index) => valuesPassed(argument)
|
|
152
|
+
.filter((value) => spellings.has(value.name.getText()))
|
|
153
|
+
.map((value) => ({ site, argument: index, value }))));
|
|
154
|
+
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
155
|
+
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
|
|
156
|
+
const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
|
|
157
|
+
return candidates.flatMap((one) => {
|
|
158
|
+
const symbol = one.value.shorthand
|
|
159
|
+
? project.shorthandValueAt(project.handle(one.value.name))
|
|
160
|
+
: symbolOf.get(one);
|
|
161
|
+
const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
|
|
162
|
+
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The destinations of one project's calls. A call is one at every argument where its
|
|
167
|
+
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
168
|
+
* `unknown` or `any` where its callee is declared outside the analysis; and, where its
|
|
169
|
+
* callee is a function of the project, at each parameter the function hands on to a
|
|
170
|
+
* destination, holding back the widest of what those destinations hold back, which is
|
|
171
|
+
* settled by widening until no parameter moves.
|
|
172
|
+
*/
|
|
173
|
+
function destinations(input, chains, walked) {
|
|
174
|
+
const { project, held } = input;
|
|
175
|
+
const checker = project.checker;
|
|
176
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
177
|
+
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
178
|
+
const callees = resolvedTargets(project, walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []));
|
|
179
|
+
const calleeOf = (call) => {
|
|
180
|
+
const name = calleeName(call.expression);
|
|
181
|
+
return name === undefined ? undefined : callees.get(name);
|
|
182
|
+
};
|
|
183
|
+
const erased = erasedParameters(project, walked.parameters);
|
|
184
|
+
const byFunction = new Map();
|
|
185
|
+
for (const parameter of erased.values()) {
|
|
186
|
+
byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
|
|
187
|
+
}
|
|
188
|
+
const outside = (target) => target.declarations.length > 0 &&
|
|
189
|
+
target.declarations.every((handle) => !own.has(handle.path) &&
|
|
190
|
+
!input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)));
|
|
191
|
+
const erasedArguments = new Map();
|
|
192
|
+
const erasedAt = (call, argument) => {
|
|
193
|
+
let indexes = erasedArguments.get(call);
|
|
194
|
+
if (indexes === undefined) {
|
|
195
|
+
const signature = checker.getResolvedSignature(call);
|
|
196
|
+
indexes = new Set(call.arguments.flatMap((_unused, index) => {
|
|
197
|
+
const type = signature === undefined ? undefined : checker.getParameterType(signature, index);
|
|
198
|
+
return type !== undefined && (type.flags & ERASED) !== 0 ? [index] : [];
|
|
199
|
+
}));
|
|
200
|
+
erasedArguments.set(call, indexes);
|
|
201
|
+
}
|
|
202
|
+
return indexes.has(argument);
|
|
203
|
+
};
|
|
204
|
+
const forwards = new Map();
|
|
205
|
+
const keyOf = (parameter) => `${parameter.fn}\u0000${String(parameter.index)}`;
|
|
206
|
+
const at = (call, argument) => {
|
|
207
|
+
const target = calleeOf(call);
|
|
208
|
+
if (target === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const entry = named.find((one) => names(one, target, chains));
|
|
212
|
+
if (entry !== undefined) {
|
|
213
|
+
return { reach: "properties", callee: spelledEntry(entry.entry, held) };
|
|
214
|
+
}
|
|
215
|
+
const declared = chains.declarationsOf(target);
|
|
216
|
+
if (declared.length === 0) {
|
|
217
|
+
return outside(target) && erasedAt(call, argument)
|
|
218
|
+
? { reach: "conversions", callee: call.expression.getText().replace(/\s+/gu, "") }
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
for (const fn of declared) {
|
|
222
|
+
const parameter = (byFunction.get(fn) ?? []).find((one) => one.index === argument || (one.rest && one.index <= argument));
|
|
223
|
+
const reach = parameter === undefined ? undefined : forwards.get(keyOf(parameter));
|
|
224
|
+
if (reach !== undefined) {
|
|
225
|
+
return { reach, callee: held.symbols.find((symbol) => symbol.id === fn)?.name ?? fn };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
};
|
|
230
|
+
const forwarded = handedOn(project, walked.sites, erased);
|
|
231
|
+
for (let moved = true; moved;) {
|
|
232
|
+
moved = false;
|
|
233
|
+
for (const one of forwarded) {
|
|
234
|
+
const key = keyOf(one.parameter);
|
|
235
|
+
const before = forwards.get(key);
|
|
236
|
+
const after = wider(before, at(one.site.call, one.argument)?.reach);
|
|
237
|
+
if (after !== undefined && after !== before) {
|
|
238
|
+
forwards.set(key, after);
|
|
239
|
+
moved = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const mayReceive = (call) => {
|
|
244
|
+
const target = calleeOf(call);
|
|
245
|
+
if (target === undefined) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const declared = chains.declarationsOf(target);
|
|
249
|
+
if (named.some((one) => names(one, target, chains))) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (declared.length === 0) {
|
|
253
|
+
return outside(target);
|
|
254
|
+
}
|
|
255
|
+
return declared.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
256
|
+
};
|
|
257
|
+
return { at, mayReceive };
|
|
258
|
+
}
|
|
259
|
+
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
260
|
+
function flowsOf(project, held, sites, to) {
|
|
261
|
+
const reach = valueReach(project, held);
|
|
262
|
+
const arguments_ = sites.flatMap((site) => to.mayReceive(site.call)
|
|
263
|
+
? site.call.arguments.flatMap((argument, index) => {
|
|
264
|
+
const value = evaluated(argument);
|
|
265
|
+
return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
|
|
266
|
+
})
|
|
267
|
+
: []);
|
|
268
|
+
const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
|
|
269
|
+
const types = typesAt(project, asked.map((one) => one.value));
|
|
270
|
+
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
271
|
+
return arguments_.flatMap((one) => {
|
|
272
|
+
const type = typeOf.get(one.value);
|
|
273
|
+
let classes = new Set();
|
|
274
|
+
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
275
|
+
classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
|
|
276
|
+
}
|
|
277
|
+
else if (type !== undefined) {
|
|
278
|
+
classes = reach.classesOf(type);
|
|
279
|
+
}
|
|
280
|
+
const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
|
|
281
|
+
return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The serialization-contract detector: each property of each class whose values reach
|
|
286
|
+
* a destination, and the two conversion methods where the destination is outside the
|
|
287
|
+
* analysis, recorded at the argument with the callee named.
|
|
288
|
+
*/
|
|
289
|
+
export function serializationContract(input) {
|
|
290
|
+
const { project, held, targetRoot } = input;
|
|
291
|
+
const walked = walkProject(project, held);
|
|
292
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, aliasChains(project, held), walked));
|
|
293
|
+
if (flows.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
|
|
297
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
298
|
+
const found = [];
|
|
299
|
+
for (const flow of flows) {
|
|
300
|
+
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
301
|
+
const detail = `passed to ${flow.to.callee}`;
|
|
302
|
+
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
303
|
+
const symbol = byId.get(member.id);
|
|
304
|
+
const isProperty = (member.flags & SymbolFlags.Property) !== 0;
|
|
305
|
+
const converts = flow.to.reach === "conversions" &&
|
|
306
|
+
(member.flags & SymbolFlags.Method) !== 0 &&
|
|
307
|
+
symbol !== undefined &&
|
|
308
|
+
CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
|
|
309
|
+
if (isProperty || converts) {
|
|
310
|
+
found.push({ id: member.id, detail, site });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return found;
|
|
315
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The classes of the target a value of one type carries, as a serializer walking the
|
|
3
|
+
* value reaches them: through union and intersection constituents, array and tuple
|
|
4
|
+
* elements, and the data members of the target's classes and of object types the
|
|
5
|
+
* target writes. The walk stops at `any`, `unknown`, a type parameter, a function and
|
|
6
|
+
* an interface or class of another program, so an instance held only there is missed.
|
|
7
|
+
*/
|
|
8
|
+
import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
|
|
9
|
+
import { classNames, typesAt } from "./class-members.js";
|
|
10
|
+
/** The object types whose members the walk reads whatever declares them. */
|
|
11
|
+
const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
12
|
+
/** The symbols of an anonymous object type the target writes as a type. */
|
|
13
|
+
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
14
|
+
/** The global array types, whose element type is what an array value carries. */
|
|
15
|
+
function arrayTargets(project) {
|
|
16
|
+
const checker = project.checker;
|
|
17
|
+
return new Set(["Array", "ReadonlyArray"].flatMap((name) => {
|
|
18
|
+
const symbol = checker.resolveName(name, SymbolFlags.Type, undefined, false);
|
|
19
|
+
return symbol === undefined ? [] : [checker.getDeclaredTypeOfSymbol(symbol).id];
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The walk over one project's types, against the classes of `held`, the same
|
|
24
|
+
* project's inventory.
|
|
25
|
+
*/
|
|
26
|
+
export function valueReach(project, held) {
|
|
27
|
+
const checker = project.checker;
|
|
28
|
+
const names = [...classNames(project, held)];
|
|
29
|
+
const declared = typesAt(project, names.map(([, node]) => node));
|
|
30
|
+
const classOfTarget = new Map();
|
|
31
|
+
names.forEach(([id], index) => {
|
|
32
|
+
const type = declared[index];
|
|
33
|
+
if (type !== undefined) {
|
|
34
|
+
classOfTarget.set(type.id, id);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const arrays = arrayTargets(project);
|
|
38
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
39
|
+
const children = new Map();
|
|
40
|
+
/** The generic target of a type reference, and the type itself otherwise. */
|
|
41
|
+
const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 && type.isTypeReference()
|
|
42
|
+
? type.getTarget()
|
|
43
|
+
: type;
|
|
44
|
+
const classOf = (type) => {
|
|
45
|
+
if (!type.isObjectType()) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return classOfTarget.get(type.id) ?? classOfTarget.get(targetOf(type).id);
|
|
49
|
+
};
|
|
50
|
+
/** Whether the target writes the object type, so its members are what the value holds. */
|
|
51
|
+
const isWritten = (type) => {
|
|
52
|
+
if ((type.objectFlags & WRITTEN) !== 0) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const symbol = type.getSymbol();
|
|
56
|
+
if (symbol === undefined) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if ((type.objectFlags & ObjectFlags.Anonymous) !== 0) {
|
|
60
|
+
return (symbol.flags & LITERAL_SYMBOLS) !== 0;
|
|
61
|
+
}
|
|
62
|
+
return ((symbol.flags & SymbolFlags.Interface) !== 0 &&
|
|
63
|
+
symbol.declarations.every((handle) => own.has(handle.path)));
|
|
64
|
+
};
|
|
65
|
+
/** The types a value of one object type holds in its data members. */
|
|
66
|
+
const memberTypes = (type) => {
|
|
67
|
+
const properties = checker
|
|
68
|
+
.getPropertiesOfType(type)
|
|
69
|
+
.filter((property) => (property.flags & SymbolFlags.Property) !== 0);
|
|
70
|
+
const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
|
|
71
|
+
return [
|
|
72
|
+
...types.filter((held) => held !== undefined),
|
|
73
|
+
...checker.getIndexInfosOfType(type).map((info) => info.valueType),
|
|
74
|
+
];
|
|
75
|
+
};
|
|
76
|
+
/** The types a value of one type holds, read once per type. */
|
|
77
|
+
const childrenOf = (type) => {
|
|
78
|
+
const known = children.get(type.id);
|
|
79
|
+
if (known !== undefined) {
|
|
80
|
+
return known;
|
|
81
|
+
}
|
|
82
|
+
let found = [];
|
|
83
|
+
if (type.isUnionType() || type.isIntersectionType()) {
|
|
84
|
+
found = type.getTypes();
|
|
85
|
+
}
|
|
86
|
+
else if (type.isObjectType()) {
|
|
87
|
+
const target = targetOf(type);
|
|
88
|
+
const isList = type.isTypeReference() &&
|
|
89
|
+
(arrays.has(target.id) ||
|
|
90
|
+
(target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
|
|
91
|
+
if (classOf(type) !== undefined || (!isList && isWritten(type))) {
|
|
92
|
+
found = memberTypes(type);
|
|
93
|
+
}
|
|
94
|
+
else if (isList && type.isTypeReference()) {
|
|
95
|
+
found = checker.getTypeArguments(type);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
children.set(type.id, found);
|
|
99
|
+
return found;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
classesOf: (type) => {
|
|
103
|
+
const found = new Set();
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const pending = [type];
|
|
106
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
107
|
+
if (seen.has(next.id)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
seen.add(next.id);
|
|
111
|
+
const owner = classOf(next);
|
|
112
|
+
if (owner !== undefined) {
|
|
113
|
+
found.add(owner);
|
|
114
|
+
}
|
|
115
|
+
pending.push(...childrenOf(next));
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"fast-check": "4.10.2",
|
|
38
38
|
"prettier": "3.9.9",
|
|
39
39
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
40
|
-
"typescript-eslint": "8.
|
|
40
|
+
"typescript-eslint": "8.71.0",
|
|
41
41
|
"vitest": "5.0.2"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
package/src/analysis.ts
CHANGED
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
} from "./exempt.ts";
|
|
23
23
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
24
24
|
import type { Finding } from "./finding.ts";
|
|
25
|
+
import { frameworkLifecycle } from "./framework-lifecycle.ts";
|
|
25
26
|
import type { Host } from "./host.ts";
|
|
27
|
+
import { injectionContainer } from "./injection-container.ts";
|
|
26
28
|
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
27
29
|
import { inventory, type Inventory } from "./inventory.ts";
|
|
28
30
|
import { readManifest } from "./manifest.ts";
|
|
@@ -33,6 +35,7 @@ import { reflectiveLookup } from "./reflective-lookup.ts";
|
|
|
33
35
|
import { references } from "./references.ts";
|
|
34
36
|
import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
|
|
35
37
|
import type { Scope } from "./scope.ts";
|
|
38
|
+
import { serializationContract } from "./serialization-contract.ts";
|
|
36
39
|
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
37
40
|
import type { Exemption, SweepInput } from "./sweep.ts";
|
|
38
41
|
import { readTemplates, templateField } from "./template-field.ts";
|
|
@@ -222,6 +225,9 @@ const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
|
|
|
222
225
|
["template-field", templateField],
|
|
223
226
|
["reflective-lookup", reflectiveLookup],
|
|
224
227
|
["decorator", decorator],
|
|
228
|
+
["injection-container", injectionContainer],
|
|
229
|
+
["framework-lifecycle", frameworkLifecycle],
|
|
230
|
+
["serialization-contract", serializationContract],
|
|
225
231
|
]);
|
|
226
232
|
|
|
227
233
|
/**
|
|
@@ -247,6 +253,7 @@ export function runSweep(
|
|
|
247
253
|
const templates = disabled.has("template-field")
|
|
248
254
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
249
255
|
: readTemplates(host, targetRoot, config.analysis);
|
|
256
|
+
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
250
257
|
const read = readProjects(engine, host, scope, config, (project, projectRead) => {
|
|
251
258
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
252
259
|
testFiles: config.ts.testFiles,
|
|
@@ -259,7 +266,7 @@ export function runSweep(
|
|
|
259
266
|
testFiles: resolved.testFilePaths,
|
|
260
267
|
};
|
|
261
268
|
const exempt = computeExemptions(
|
|
262
|
-
{ project, held: projectRead.held, targetRoot, templates },
|
|
269
|
+
{ project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
|
|
263
270
|
detectors,
|
|
264
271
|
{
|
|
265
272
|
disabled,
|