@cplieger/deadset-ts 3.1.0 → 3.3.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 +3 -3
- package/dist/src/analysis.js +39 -13
- package/dist/src/decorator.js +64 -0
- package/dist/src/exempt-classes.js +83 -0
- package/dist/src/exempt.js +152 -0
- package/dist/src/interface-satisfaction.js +310 -0
- package/dist/src/member-names.js +76 -0
- package/dist/src/references.js +22 -3
- package/dist/src/reflective-lookup.js +96 -0
- package/dist/src/run.js +33 -1
- package/dist/src/sweep.js +35 -25
- package/dist/src/template-field.js +173 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/analysis.ts +59 -14
- package/src/decorator.ts +74 -0
- package/src/exempt-classes.ts +132 -0
- package/src/exempt.ts +226 -0
- package/src/interface-satisfaction.ts +417 -0
- package/src/member-names.ts +89 -0
- package/src/references.ts +41 -6
- package/src/reflective-lookup.ts +127 -0
- package/src/run.ts +39 -1
- package/src/sweep.ts +60 -25
- package/src/template-field.ts +214 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interface-satisfaction class: a class member is held back when a value of the
|
|
3
|
+
* class reaches a position the checker types as an interface requiring a member of
|
|
4
|
+
* that name, so a caller reaches the member through the interface while no name in
|
|
5
|
+
* the program resolves to it.
|
|
6
|
+
*
|
|
7
|
+
* A value reaches such a position as an `implements` entry, the initializer of a
|
|
8
|
+
* declaration whose type is written, an assignment, an argument, a returned value, an
|
|
9
|
+
* array element, an object property, or the operand of `as` or `satisfies`, read
|
|
10
|
+
* through parentheses, conditionals and the operators whose result is an operand.
|
|
11
|
+
* `this` in an instance member is a value of its class. Types are asked in batches;
|
|
12
|
+
* the contextual lookup has none, so it is asked only of a value whose type is a class
|
|
13
|
+
* of the target, and each distinct pair, interface and class is asked once.
|
|
14
|
+
*/
|
|
15
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isConditionalExpression, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isIdentifier, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
16
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
17
|
+
import { aliasChains } from "./alias-chain.js";
|
|
18
|
+
import { nodeKey } from "./inventory.js";
|
|
19
|
+
import { renderPosition } from "./position.js";
|
|
20
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
21
|
+
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
22
|
+
const STORES = new Set([
|
|
23
|
+
SyntaxKind.EqualsToken,
|
|
24
|
+
SyntaxKind.QuestionQuestionEqualsToken,
|
|
25
|
+
SyntaxKind.BarBarEqualsToken,
|
|
26
|
+
SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
27
|
+
]);
|
|
28
|
+
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
29
|
+
function isStatic(node) {
|
|
30
|
+
const flags = node.modifierFlags ?? 0;
|
|
31
|
+
return (flags & ModifierFlags.Static) !== 0;
|
|
32
|
+
}
|
|
33
|
+
/** Whether one node is a method or an accessor of an object literal, whose `this` is the literal. */
|
|
34
|
+
function isObjectLiteralMethod(node) {
|
|
35
|
+
return ((isMethodDeclaration(node) ||
|
|
36
|
+
isGetAccessorDeclaration(node) ||
|
|
37
|
+
isSetAccessorDeclaration(node)) &&
|
|
38
|
+
isObjectLiteralExpression(node.parent));
|
|
39
|
+
}
|
|
40
|
+
/** Whether a declaration writes its type, so its initializer is read in that type's context. */
|
|
41
|
+
function writesType(node) {
|
|
42
|
+
return node.type !== undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The values one written expression hands on: the expression itself, or what it
|
|
46
|
+
* evaluates to through parentheses, a conditional and the operators whose result is
|
|
47
|
+
* one of their operands.
|
|
48
|
+
*/
|
|
49
|
+
function valuesOf(expression) {
|
|
50
|
+
if (isParenthesizedExpression(expression)) {
|
|
51
|
+
return valuesOf(expression.expression);
|
|
52
|
+
}
|
|
53
|
+
if (isConditionalExpression(expression)) {
|
|
54
|
+
return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
|
|
55
|
+
}
|
|
56
|
+
if (isBinaryExpression(expression)) {
|
|
57
|
+
const operator = expression.operatorToken.kind;
|
|
58
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
59
|
+
return [...valuesOf(expression.left), ...valuesOf(expression.right)];
|
|
60
|
+
}
|
|
61
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
|
|
62
|
+
return valuesOf(expression.right);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [expression];
|
|
66
|
+
}
|
|
67
|
+
/** The expressions one node hands to a position typed by its context. */
|
|
68
|
+
function handedOn(node) {
|
|
69
|
+
if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
|
|
70
|
+
node.initializer !== undefined &&
|
|
71
|
+
writesType(node)) {
|
|
72
|
+
return [node.initializer];
|
|
73
|
+
}
|
|
74
|
+
if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
|
|
75
|
+
return [node.right];
|
|
76
|
+
}
|
|
77
|
+
if (isCallExpression(node) || isNewExpression(node)) {
|
|
78
|
+
return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
|
|
79
|
+
}
|
|
80
|
+
if (isReturnStatement(node)) {
|
|
81
|
+
return node.expression === undefined ? [] : [node.expression];
|
|
82
|
+
}
|
|
83
|
+
if (isArrowFunction(node)) {
|
|
84
|
+
return node.body.kind === SyntaxKind.Block ? [] : [node.body];
|
|
85
|
+
}
|
|
86
|
+
if (isArrayLiteralExpression(node)) {
|
|
87
|
+
return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
|
|
88
|
+
}
|
|
89
|
+
if (isPropertyAssignment(node)) {
|
|
90
|
+
return [node.initializer];
|
|
91
|
+
}
|
|
92
|
+
if (isShorthandPropertyAssignment(node)) {
|
|
93
|
+
return isIdentifier(node.name) ? [node.name] : [];
|
|
94
|
+
}
|
|
95
|
+
if (isAsExpression(node) || isSatisfiesExpression(node)) {
|
|
96
|
+
return [node.expression];
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The classes, the `implements` entries and the flowing values of one project's own
|
|
102
|
+
* files. `this` is the class an instance member belongs to, so a function that is not
|
|
103
|
+
* an arrow, a static member and a static block each stop it.
|
|
104
|
+
*/
|
|
105
|
+
function walk(files, declarations) {
|
|
106
|
+
const classes = [];
|
|
107
|
+
const clauses = [];
|
|
108
|
+
const flows = [];
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const visit = (node, self) => {
|
|
111
|
+
let inner = self;
|
|
112
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
113
|
+
const id = declarations.get(nodeKey(file, node));
|
|
114
|
+
const name = node.name;
|
|
115
|
+
const owner = id === undefined || name === undefined
|
|
116
|
+
? undefined
|
|
117
|
+
: { id, name, generic: node.typeParameters !== undefined };
|
|
118
|
+
if (owner !== undefined) {
|
|
119
|
+
classes.push(owner);
|
|
120
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
121
|
+
if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
|
|
122
|
+
clauses.push(...clause.types.map((entry) => ({ owner, entry })));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
inner = owner;
|
|
127
|
+
}
|
|
128
|
+
else if (isFunctionDeclaration(node) ||
|
|
129
|
+
isFunctionExpression(node) ||
|
|
130
|
+
isClassStaticBlockDeclaration(node) ||
|
|
131
|
+
isStatic(node) ||
|
|
132
|
+
isObjectLiteralMethod(node)) {
|
|
133
|
+
inner = undefined;
|
|
134
|
+
}
|
|
135
|
+
for (const written of handedOn(node)) {
|
|
136
|
+
for (const value of valuesOf(written)) {
|
|
137
|
+
flows.push({
|
|
138
|
+
value,
|
|
139
|
+
self: value.kind === SyntaxKind.ThisKeyword ? inner : undefined,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
node.forEachChild((child) => {
|
|
144
|
+
visit(child, inner);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
file.forEachChild((child) => {
|
|
148
|
+
visit(child, undefined);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { classes, clauses, flows };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The interface-satisfaction detector: for each class type that reaches an interface
|
|
155
|
+
* type it is assignable to, the class's members whose names the interface's members
|
|
156
|
+
* carry, each recorded at the place the value flows.
|
|
157
|
+
*/
|
|
158
|
+
export function interfaceSatisfaction(input) {
|
|
159
|
+
const { project, held, targetRoot } = input;
|
|
160
|
+
const checker = project.checker;
|
|
161
|
+
const files = project.ownSourceFiles();
|
|
162
|
+
const { classes, clauses, flows } = walk(files, held.declarations);
|
|
163
|
+
if (classes.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
// One list, so the batches are capped runs of the whole project rather than of a
|
|
167
|
+
// file: a class's name, an `implements` entry and a value are each one question.
|
|
168
|
+
const asked = [
|
|
169
|
+
...classes.map((owner) => owner.name),
|
|
170
|
+
...clauses.map((clause) => clause.entry),
|
|
171
|
+
...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
|
|
172
|
+
];
|
|
173
|
+
const typeAt = new Map();
|
|
174
|
+
for (let from = 0; from < asked.length; from += DEFAULT_BATCH_CAP) {
|
|
175
|
+
const run = asked.slice(from, from + DEFAULT_BATCH_CAP);
|
|
176
|
+
checker.getTypeAtLocation(run).forEach((type, index) => {
|
|
177
|
+
const node = run[index];
|
|
178
|
+
if (type !== undefined && node !== undefined) {
|
|
179
|
+
typeAt.set(node, type);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const classOfType = new Map();
|
|
184
|
+
const instanceOf = new Map();
|
|
185
|
+
for (const owner of classes) {
|
|
186
|
+
const type = typeAt.get(owner.name);
|
|
187
|
+
if (type !== undefined) {
|
|
188
|
+
classOfType.set(type.id, owner);
|
|
189
|
+
instanceOf.set(owner.id, type);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const instantiates = classes.some((owner) => owner.generic);
|
|
193
|
+
const constituents = new Map();
|
|
194
|
+
const partsOf = (type) => {
|
|
195
|
+
if (!type.isUnionType() && !type.isIntersectionType()) {
|
|
196
|
+
return [type];
|
|
197
|
+
}
|
|
198
|
+
let parts = constituents.get(type.id);
|
|
199
|
+
if (parts === undefined) {
|
|
200
|
+
parts = type.getTypes();
|
|
201
|
+
constituents.set(type.id, parts);
|
|
202
|
+
}
|
|
203
|
+
return parts;
|
|
204
|
+
};
|
|
205
|
+
const targets = new Map();
|
|
206
|
+
/** The classes of the target one value's type is an instance of, with the instance type. */
|
|
207
|
+
const classesOf = (type) => partsOf(type).flatMap((part) => {
|
|
208
|
+
const direct = classOfType.get(part.id);
|
|
209
|
+
if (direct !== undefined) {
|
|
210
|
+
return [{ owner: direct, type: part }];
|
|
211
|
+
}
|
|
212
|
+
if (!instantiates || !part.isTypeReference()) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
if (!targets.has(part.id)) {
|
|
216
|
+
targets.set(part.id, classOfType.get(part.getTarget().id));
|
|
217
|
+
}
|
|
218
|
+
const generic = targets.get(part.id);
|
|
219
|
+
return generic === undefined ? [] : [{ owner: generic, type: part }];
|
|
220
|
+
});
|
|
221
|
+
const interfaceNames = new Map();
|
|
222
|
+
/** The interface types one position's type is or holds, each with the interface's name. */
|
|
223
|
+
const interfacesOf = (type) => partsOf(type).flatMap((part) => {
|
|
224
|
+
if (!interfaceNames.has(part.id)) {
|
|
225
|
+
const symbol = part.isObjectType() ? part.getSymbol() : undefined;
|
|
226
|
+
interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
|
|
227
|
+
? symbol.name
|
|
228
|
+
: undefined);
|
|
229
|
+
}
|
|
230
|
+
const name = interfaceNames.get(part.id);
|
|
231
|
+
return name === undefined ? [] : [{ type: part, name }];
|
|
232
|
+
});
|
|
233
|
+
const pairs = new Map();
|
|
234
|
+
const reach = (owner, source, target, name, site) => {
|
|
235
|
+
const key = `${String(source.id)}:${String(target.id)}`;
|
|
236
|
+
const pair = pairs.get(key);
|
|
237
|
+
if (pair === undefined) {
|
|
238
|
+
pairs.set(key, { owner, source, target, name, sites: [site] });
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
pair.sites.push(site);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
for (const clause of clauses) {
|
|
245
|
+
const source = instanceOf.get(clause.owner.id);
|
|
246
|
+
const target = typeAt.get(clause.entry);
|
|
247
|
+
if (source === undefined || target === undefined) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
for (const reached of interfacesOf(target)) {
|
|
251
|
+
reach(clause.owner, source, reached.type, reached.name, clause.entry);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const flow of flows) {
|
|
255
|
+
const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
|
|
256
|
+
const valueType = typeAt.get(flow.value);
|
|
257
|
+
const sources = flow.self !== undefined && self !== undefined
|
|
258
|
+
? [{ owner: flow.self, type: self }]
|
|
259
|
+
: valueType === undefined
|
|
260
|
+
? []
|
|
261
|
+
: classesOf(valueType);
|
|
262
|
+
if (sources.length === 0) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const position = checker.getContextualType(flow.value);
|
|
266
|
+
if (position === undefined) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
for (const reached of interfacesOf(position)) {
|
|
270
|
+
for (const source of sources) {
|
|
271
|
+
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const chains = aliasChains(project, held);
|
|
276
|
+
const required = new Map();
|
|
277
|
+
const members = new Map();
|
|
278
|
+
const found = [];
|
|
279
|
+
for (const pair of pairs.values()) {
|
|
280
|
+
if (!checker.isTypeAssignableTo(pair.source, pair.target)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
let names = required.get(pair.target.id);
|
|
284
|
+
if (names === undefined) {
|
|
285
|
+
names = new Set(checker.getPropertiesOfType(pair.target).map((property) => property.escapedName));
|
|
286
|
+
required.set(pair.target.id, names);
|
|
287
|
+
}
|
|
288
|
+
let answering = members.get(pair.owner.id);
|
|
289
|
+
if (answering === undefined) {
|
|
290
|
+
const instance = instanceOf.get(pair.owner.id) ?? pair.source;
|
|
291
|
+
answering = new Map(checker
|
|
292
|
+
.getPropertiesOfType(instance)
|
|
293
|
+
.map((property) => [property.escapedName, chains.declarationsOf(property)]));
|
|
294
|
+
members.set(pair.owner.id, answering);
|
|
295
|
+
}
|
|
296
|
+
const detail = `satisfies ${pair.name}`;
|
|
297
|
+
for (const name of names) {
|
|
298
|
+
for (const id of answering.get(name) ?? []) {
|
|
299
|
+
for (const site of pair.sites) {
|
|
300
|
+
found.push({
|
|
301
|
+
id,
|
|
302
|
+
detail,
|
|
303
|
+
site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return found;
|
|
310
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members of one project's inventory by the name each is written with, for the
|
|
3
|
+
* classes whose evidence is a name rather than a type relation: a template and a
|
|
4
|
+
* string key both hold a name and nothing else, so each holds back every member of
|
|
5
|
+
* that name its own mechanism can reach.
|
|
6
|
+
*/
|
|
7
|
+
import { isIdentifier, isNumericLiteral, isPrivateIdentifier, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { nodeKey } from "./inventory.js";
|
|
9
|
+
/** The kinds of declaration a member name can name. */
|
|
10
|
+
const MEMBER_KINDS = new Set([
|
|
11
|
+
"method",
|
|
12
|
+
"class-member",
|
|
13
|
+
"interface-method",
|
|
14
|
+
"type-member",
|
|
15
|
+
"enum-member",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* The name a member declaration is written with: an identifier's text, a private
|
|
19
|
+
* name's text without its `#`, a string key's value and a numeric key's text. A
|
|
20
|
+
* private name is indexed under the name a template or a string would spell, so the
|
|
21
|
+
* rule that no such evidence holds it back is the framework's alone. A computed key
|
|
22
|
+
* names nothing a string can be equal to before the program runs.
|
|
23
|
+
*/
|
|
24
|
+
function writtenName(name) {
|
|
25
|
+
if (name === undefined) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (isPrivateIdentifier(name)) {
|
|
29
|
+
return name.text.replace(/^#/u, "");
|
|
30
|
+
}
|
|
31
|
+
if (isIdentifier(name)) {
|
|
32
|
+
return name.text;
|
|
33
|
+
}
|
|
34
|
+
if (isStringLiteral(name) || isNumericLiteral(name)) {
|
|
35
|
+
return name.text;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every member the inventory holds, keyed by the name it is written with, each
|
|
41
|
+
* member once and in inventory order. A getter and a setter of one name are one
|
|
42
|
+
* member.
|
|
43
|
+
*/
|
|
44
|
+
export function memberNames(files, held) {
|
|
45
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
46
|
+
const named = new Map();
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const visit = (node) => {
|
|
49
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
50
|
+
const symbol = id === undefined ? undefined : byId.get(id);
|
|
51
|
+
if (symbol !== undefined && MEMBER_KINDS.has(symbol.kind)) {
|
|
52
|
+
const name = writtenName(node.name);
|
|
53
|
+
if (name !== undefined) {
|
|
54
|
+
named.set(symbol.id, name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
node.forEachChild(visit);
|
|
58
|
+
};
|
|
59
|
+
visit(file);
|
|
60
|
+
}
|
|
61
|
+
const index = new Map();
|
|
62
|
+
for (const symbol of held.symbols) {
|
|
63
|
+
const name = named.get(symbol.id);
|
|
64
|
+
if (name === undefined) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const members = index.get(name);
|
|
68
|
+
if (members === undefined) {
|
|
69
|
+
index.set(name, [symbol]);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
members.push(symbol);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return index;
|
|
76
|
+
}
|
package/dist/src/references.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
1
|
+
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { globExpression } from "./glob.js";
|
|
4
4
|
import { nodeKey } from "./inventory.js";
|
|
@@ -207,6 +207,7 @@ function sitesOf(file, declarations, fileId) {
|
|
|
207
207
|
const found = [];
|
|
208
208
|
const links = [];
|
|
209
209
|
const evaluations = [];
|
|
210
|
+
const decorated = [];
|
|
210
211
|
const writes = new Set();
|
|
211
212
|
const declared = new Set();
|
|
212
213
|
const shorthands = new Map();
|
|
@@ -251,12 +252,17 @@ function sitesOf(file, declarations, fileId) {
|
|
|
251
252
|
const outer = enclosing;
|
|
252
253
|
if (held !== undefined) {
|
|
253
254
|
enclosing = held;
|
|
255
|
+
for (const modifier of node.modifiers ?? []) {
|
|
256
|
+
if (isDecorator(modifier)) {
|
|
257
|
+
decorated.push({ decorator: modifier, declaration: held });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
254
260
|
}
|
|
255
261
|
node.forEachChild(visit);
|
|
256
262
|
enclosing = outer;
|
|
257
263
|
};
|
|
258
264
|
file.forEachChild(visit);
|
|
259
|
-
return { uses: found, links, evaluations };
|
|
265
|
+
return { uses: found, links, evaluations, decorated };
|
|
260
266
|
}
|
|
261
267
|
/**
|
|
262
268
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
@@ -336,7 +342,20 @@ export function references(project, held, targetRoot, options) {
|
|
|
336
342
|
if (test) {
|
|
337
343
|
tests.push(path);
|
|
338
344
|
}
|
|
339
|
-
const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
345
|
+
const { uses, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
346
|
+
// A decorator is written inside the declaration it is attached to and receives it,
|
|
347
|
+
// so the declaration is used where the decorator is written, whatever the
|
|
348
|
+
// decorator does with it.
|
|
349
|
+
for (const { decorator, declaration } of decorated) {
|
|
350
|
+
found.push({
|
|
351
|
+
from: declaration,
|
|
352
|
+
to: declaration,
|
|
353
|
+
position: renderPosition(file, targetRoot, decorator.getStart()),
|
|
354
|
+
use: "decorator",
|
|
355
|
+
resolution: "syntax",
|
|
356
|
+
test,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
340
359
|
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
341
360
|
// the uses and the evaluated modules' specifiers last, each node once: a bare star
|
|
342
361
|
// re-export's specifier is a link and an evaluation both. A shorthand is left out
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reflective-lookup class: a member is held back, at the lowest confidence, when
|
|
3
|
+
* a string literal equal to its name is the key of an element access or the property
|
|
4
|
+
* key of a `Reflect.get`, `Reflect.set` or `Reflect.has` call, because the member is
|
|
5
|
+
* then read by a name the program computes nowhere the reference pass can follow.
|
|
6
|
+
*
|
|
7
|
+
* The evidence is a string beside a lookup rather than a type relation, so the
|
|
8
|
+
* string reaches every member of that name on any type, static members included,
|
|
9
|
+
* and every record names the string's own position. A key that is not literal text
|
|
10
|
+
* holds nothing back: a computed name is the case no exact rule serves. `Reflect` is
|
|
11
|
+
* the global one only: a local declaration of that name is a different object.
|
|
12
|
+
*/
|
|
13
|
+
import { isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
14
|
+
import { memberNames } from "./member-names.js";
|
|
15
|
+
import { renderPosition } from "./position.js";
|
|
16
|
+
/** The `Reflect` methods whose second argument is a property key. */
|
|
17
|
+
const REFLECT_KEYED = new Set(["get", "set", "has"]);
|
|
18
|
+
/** The text one key denotes, where it is literal text. */
|
|
19
|
+
function literalText(node) {
|
|
20
|
+
if (node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))) {
|
|
21
|
+
return node.text;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Every lookup one file writes with a literal key, in source order. */
|
|
26
|
+
function lookupsOf(file) {
|
|
27
|
+
const found = [];
|
|
28
|
+
const visit = (node) => {
|
|
29
|
+
if (isElementAccessExpression(node)) {
|
|
30
|
+
const name = literalText(node.argumentExpression);
|
|
31
|
+
if (name !== undefined) {
|
|
32
|
+
found.push({
|
|
33
|
+
key: node.argumentExpression,
|
|
34
|
+
name,
|
|
35
|
+
callee: `[${JSON.stringify(name)}]`,
|
|
36
|
+
reflect: undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
41
|
+
const target = node.expression.expression;
|
|
42
|
+
const method = node.expression.name;
|
|
43
|
+
const key = node.arguments[1];
|
|
44
|
+
const name = literalText(key);
|
|
45
|
+
if (isIdentifier(target) &&
|
|
46
|
+
target.text === "Reflect" &&
|
|
47
|
+
isIdentifier(method) &&
|
|
48
|
+
REFLECT_KEYED.has(method.text) &&
|
|
49
|
+
key !== undefined &&
|
|
50
|
+
name !== undefined) {
|
|
51
|
+
found.push({ key, name, callee: `Reflect.${method.text}`, reflect: target });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
node.forEachChild(visit);
|
|
55
|
+
};
|
|
56
|
+
file.forEachChild(visit);
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The reflective-lookup detector: every member whose name a literal key of a lookup
|
|
61
|
+
* spells, recorded at the key.
|
|
62
|
+
*/
|
|
63
|
+
export function reflectiveLookup(input) {
|
|
64
|
+
const { project, held, targetRoot } = input;
|
|
65
|
+
const files = project.ownSourceFiles();
|
|
66
|
+
const lookups = files.flatMap(lookupsOf);
|
|
67
|
+
if (lookups.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
// Whether each written `Reflect` is the global one, asked in one batch: a
|
|
71
|
+
// declaration of that name in one of the project's own files shadows it.
|
|
72
|
+
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
73
|
+
const reflects = lookups.flatMap((lookup) => lookup.reflect === undefined ? [] : [lookup.reflect]);
|
|
74
|
+
const global = new Set();
|
|
75
|
+
if (reflects.length > 0) {
|
|
76
|
+
project.symbolsAt(reflects.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
77
|
+
const node = reflects[index];
|
|
78
|
+
const declaredHere = symbol?.declarations.some((declaration) => ownFiles.has(declaration.path));
|
|
79
|
+
if (node !== undefined && declaredHere === false) {
|
|
80
|
+
global.add(node);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const members = memberNames(files, held);
|
|
85
|
+
const found = [];
|
|
86
|
+
for (const lookup of lookups) {
|
|
87
|
+
if (lookup.reflect !== undefined && !global.has(lookup.reflect)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const site = renderPosition(lookup.key.getSourceFile(), targetRoot, lookup.key.getStart());
|
|
91
|
+
for (const member of members.get(lookup.name) ?? []) {
|
|
92
|
+
found.push({ id: member.id, detail: `looked up by ${lookup.callee}`, site });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return found;
|
|
96
|
+
}
|
package/dist/src/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { runRoots } from "./analysis.js";
|
|
1
|
+
import { runRoots, runSweep } from "./analysis.js";
|
|
2
2
|
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
3
3
|
import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
|
|
4
|
+
import { retainedLines } from "./exempt.js";
|
|
4
5
|
import { joinPath, resolvePath } from "./paths.js";
|
|
5
6
|
import { printConfig } from "./print.js";
|
|
6
7
|
import { resolve, resolveMatrix } from "./resolve.js";
|
|
@@ -174,6 +175,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
174
175
|
if (verb === "print-roots") {
|
|
175
176
|
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
176
177
|
}
|
|
178
|
+
if (verb === "print-retained") {
|
|
179
|
+
return printRetainedVerb(args.slice(1), out, err, host, openClient);
|
|
180
|
+
}
|
|
177
181
|
if (verb !== undefined && VERBS.includes(verb)) {
|
|
178
182
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
179
183
|
return EXIT_USAGE;
|
|
@@ -276,6 +280,34 @@ function printRootsVerb(args, out, err, host, openClient) {
|
|
|
276
280
|
return refuse(error, err);
|
|
277
281
|
}
|
|
278
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Writes every declaration an exemption held back in the production sweep a report is
|
|
285
|
+
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
286
|
+
* several configurations found is one record, so the line names no configuration. The
|
|
287
|
+
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
288
|
+
* read it.
|
|
289
|
+
*/
|
|
290
|
+
function printRetainedVerb(args, out, err, host, openClient) {
|
|
291
|
+
let engine;
|
|
292
|
+
try {
|
|
293
|
+
const options = readOptions(args);
|
|
294
|
+
const { config } = resolve(inputsOf(host, options));
|
|
295
|
+
const scope = scopeOf(host, options);
|
|
296
|
+
engine = openClient(false);
|
|
297
|
+
const swept = runSweep(engine, host, scope, config, {
|
|
298
|
+
marked: [],
|
|
299
|
+
mode: { production: true },
|
|
300
|
+
});
|
|
301
|
+
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
302
|
+
out.write(`${line}\n`);
|
|
303
|
+
}
|
|
304
|
+
return EXIT_CLEAN;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
engine?.close();
|
|
308
|
+
return refuse(error, err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
279
311
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
280
312
|
function refuse(error, err) {
|
|
281
313
|
if (error instanceof ConfigError) {
|