@cplieger/deadset-ts 3.0.0 → 3.2.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/alias-chain.js +96 -0
- package/dist/src/analysis.js +32 -17
- package/dist/src/exempt-classes.js +36 -0
- package/dist/src/exempt.js +132 -0
- package/dist/src/graph.js +5 -1
- package/dist/src/matrix.js +138 -0
- package/dist/src/references.js +78 -100
- package/dist/src/roots.js +14 -37
- package/dist/src/run.js +33 -1
- package/dist/src/session.js +16 -5
- package/dist/src/sweep.js +47 -41
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/alias-chain.ts +135 -0
- package/src/analysis.ts +53 -32
- package/src/components.ts +1 -1
- package/src/exempt-classes.ts +69 -0
- package/src/exempt.ts +200 -0
- package/src/graph.ts +12 -3
- package/src/matrix.ts +212 -0
- package/src/references.ts +108 -113
- package/src/roots.ts +15 -46
- package/src/run.ts +39 -1
- package/src/session.ts +18 -5
- package/src/sweep.ts +75 -46
- package/src/version.ts +1 -1
package/dist/src/references.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isForInStatement, isForOfStatement, isNamespaceExport, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
|
-
import {
|
|
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";
|
|
2
|
+
import { aliasChains } from "./alias-chain.js";
|
|
3
3
|
import { globExpression } from "./glob.js";
|
|
4
4
|
import { nodeKey } from "./inventory.js";
|
|
5
5
|
import { byPosition, renderPosition } from "./position.js";
|
|
@@ -126,6 +126,38 @@ function markStores(node, writes) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
/** Whether one node is a string literal or a template literal with no substitution. */
|
|
130
|
+
function isLiteralText(node) {
|
|
131
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The module specifier of the module one node evaluates, if it evaluates one: an import
|
|
135
|
+
* declaration, an import-equals declaration of an external module, an export
|
|
136
|
+
* declaration that names a module, and an `import()` call whose argument is literal
|
|
137
|
+
* text. A type-only form evaluates nothing. An `import()` of a computed argument names
|
|
138
|
+
* no module this pass can know.
|
|
139
|
+
*/
|
|
140
|
+
function evaluatedBy(node) {
|
|
141
|
+
if (isImportDeclaration(node)) {
|
|
142
|
+
return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword
|
|
143
|
+
? undefined
|
|
144
|
+
: node.moduleSpecifier;
|
|
145
|
+
}
|
|
146
|
+
if (isExportDeclaration(node)) {
|
|
147
|
+
return node.isTypeOnly ? undefined : node.moduleSpecifier;
|
|
148
|
+
}
|
|
149
|
+
if (isImportEqualsDeclaration(node)) {
|
|
150
|
+
const reference = node.moduleReference;
|
|
151
|
+
return !node.isTypeOnly && isExternalModuleReference(reference)
|
|
152
|
+
? reference.expression
|
|
153
|
+
: undefined;
|
|
154
|
+
}
|
|
155
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
156
|
+
const [argument] = node.arguments;
|
|
157
|
+
return isLiteralText(argument) ? argument : undefined;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
129
161
|
/**
|
|
130
162
|
* The re-exports one node writes, if it is an export declaration. A specifier and a
|
|
131
163
|
* namespace export are declarations whose names are where they are written, so no use is
|
|
@@ -174,6 +206,7 @@ function linksOf(file, node, declarations, enclosing) {
|
|
|
174
206
|
function sitesOf(file, declarations, fileId) {
|
|
175
207
|
const found = [];
|
|
176
208
|
const links = [];
|
|
209
|
+
const evaluations = [];
|
|
177
210
|
const writes = new Set();
|
|
178
211
|
const declared = new Set();
|
|
179
212
|
const shorthands = new Map();
|
|
@@ -210,6 +243,10 @@ function sitesOf(file, declarations, fileId) {
|
|
|
210
243
|
markNames(node);
|
|
211
244
|
markStores(node, writes);
|
|
212
245
|
links.push(...linksOf(file, node, declarations, enclosing));
|
|
246
|
+
const evaluated = evaluatedBy(node);
|
|
247
|
+
if (evaluated !== undefined) {
|
|
248
|
+
evaluations.push({ node: evaluated, from: enclosing });
|
|
249
|
+
}
|
|
213
250
|
const held = declarations.get(nodeKey(file, node));
|
|
214
251
|
const outer = enclosing;
|
|
215
252
|
if (held !== undefined) {
|
|
@@ -219,7 +256,7 @@ function sitesOf(file, declarations, fileId) {
|
|
|
219
256
|
enclosing = outer;
|
|
220
257
|
};
|
|
221
258
|
file.forEachChild(visit);
|
|
222
|
-
return { uses: found, links };
|
|
259
|
+
return { uses: found, links, evaluations };
|
|
223
260
|
}
|
|
224
261
|
/**
|
|
225
262
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
@@ -247,7 +284,8 @@ function compare(a, b) {
|
|
|
247
284
|
* The order is by the referencing position, then by the declaration named, the
|
|
248
285
|
* declaration referencing it and the use. That is a total order because one name
|
|
249
286
|
* references one declaration once from one declaration, while a chain of aliases reaches
|
|
250
|
-
* several declarations at one position
|
|
287
|
+
* several declarations at one position and a bare star re-export's specifier both reads
|
|
288
|
+
* and evaluates the module it names.
|
|
251
289
|
*/
|
|
252
290
|
export function references(project, held, targetRoot, options) {
|
|
253
291
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
@@ -256,85 +294,15 @@ export function references(project, held, targetRoot, options) {
|
|
|
256
294
|
const matched = options.testFiles.map(() => 0);
|
|
257
295
|
const found = [];
|
|
258
296
|
const tests = [];
|
|
259
|
-
const
|
|
260
|
-
const steps = new Map();
|
|
297
|
+
const chains = aliasChains(project, held);
|
|
261
298
|
let batched = 0;
|
|
262
299
|
let fileBatches = 0;
|
|
263
300
|
let residueFallbacks = 0;
|
|
264
301
|
let shorthandLookups = 0;
|
|
265
|
-
let aliasSteps = 0;
|
|
266
302
|
const files = project.ownSourceFiles();
|
|
267
|
-
const
|
|
303
|
+
const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
|
|
268
304
|
/** The identifier of the declaration one node of this program is, where it is one. */
|
|
269
305
|
const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
|
|
270
|
-
/**
|
|
271
|
-
* The declarations of the inventory one symbol is.
|
|
272
|
-
*
|
|
273
|
-
* A declaration handle carries the file it is in, so the question is answered before
|
|
274
|
-
* the handle is resolved: a declaration outside the project's own files belongs to
|
|
275
|
-
* another program and is never read, which is what keeps the pass from fetching a
|
|
276
|
-
* library file to discover that the inventory does not hold what is in it.
|
|
277
|
-
*/
|
|
278
|
-
const declarationsOf = (symbol) => {
|
|
279
|
-
const ids = [];
|
|
280
|
-
for (const handle of symbol.declarations) {
|
|
281
|
-
if (!ownFiles.has(handle.path)) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
const node = project.declarationAt(handle)?.node;
|
|
285
|
-
const id = node === undefined ? undefined : declaredAt(node);
|
|
286
|
-
if (id !== undefined && !ids.includes(id)) {
|
|
287
|
-
ids.push(id);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return ids;
|
|
291
|
-
};
|
|
292
|
-
/**
|
|
293
|
-
* What one alias names, one link along, asked once per alias whatever the number of
|
|
294
|
-
* chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
|
|
295
|
-
* the condition the step asserts on: a default export of an expression is written in
|
|
296
|
-
* an alias form and names nothing declared elsewhere, so its chain ends at itself.
|
|
297
|
-
*/
|
|
298
|
-
const stepOf = (symbol) => {
|
|
299
|
-
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
300
|
-
return undefined;
|
|
301
|
-
}
|
|
302
|
-
if (steps.has(symbol.id)) {
|
|
303
|
-
return steps.get(symbol.id);
|
|
304
|
-
}
|
|
305
|
-
const next = project.aliasStepOf(symbol);
|
|
306
|
-
aliasSteps += 1;
|
|
307
|
-
steps.set(symbol.id, next);
|
|
308
|
-
return next;
|
|
309
|
-
};
|
|
310
|
-
/**
|
|
311
|
-
* The declarations one symbol names: the declaration it is, and every link of the
|
|
312
|
-
* alias chain it stands at the head of. A re-export something imports is used and so
|
|
313
|
-
* is the declaration behind it, so both are named; a link this project does not
|
|
314
|
-
* declare is stepped over rather than recorded.
|
|
315
|
-
*/
|
|
316
|
-
const targetsOf = (symbol) => {
|
|
317
|
-
const cached = reached.get(symbol.id);
|
|
318
|
-
if (cached !== undefined) {
|
|
319
|
-
return cached;
|
|
320
|
-
}
|
|
321
|
-
const targets = [];
|
|
322
|
-
const walked = new Set();
|
|
323
|
-
let at = symbol;
|
|
324
|
-
let stepped = false;
|
|
325
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
326
|
-
walked.add(at.id);
|
|
327
|
-
for (const id of declarationsOf(at)) {
|
|
328
|
-
if (!targets.some((target) => target.id === id)) {
|
|
329
|
-
targets.push({ id, stepped });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
at = stepOf(at);
|
|
333
|
-
stepped = true;
|
|
334
|
-
}
|
|
335
|
-
reached.set(symbol.id, targets);
|
|
336
|
-
return targets;
|
|
337
|
-
};
|
|
338
306
|
/**
|
|
339
307
|
* The declarations one re-export carries forward: for an alias, the first link along
|
|
340
308
|
* its chain that the inventory declares, stepping over the links it does not, such as
|
|
@@ -342,22 +310,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
342
310
|
* Each later link is a declaration of its own that references the next, so one link
|
|
343
311
|
* per re-export is what lets a chain be followed one declaration at a time.
|
|
344
312
|
*/
|
|
345
|
-
const carriedBy = (link, symbol) =>
|
|
346
|
-
if (!link.alias) {
|
|
347
|
-
return declarationsOf(symbol);
|
|
348
|
-
}
|
|
349
|
-
const walked = new Set([symbol.id]);
|
|
350
|
-
let at = stepOf(symbol);
|
|
351
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
352
|
-
walked.add(at.id);
|
|
353
|
-
const ids = declarationsOf(at);
|
|
354
|
-
if (ids.length > 0) {
|
|
355
|
-
return ids;
|
|
356
|
-
}
|
|
357
|
-
at = stepOf(at);
|
|
358
|
-
}
|
|
359
|
-
return [];
|
|
360
|
-
};
|
|
313
|
+
const carriedBy = (link, symbol) => link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
|
|
361
314
|
/** The symbol one site resolves to, asking only the accessor the site's form names. */
|
|
362
315
|
const symbolFor = (site, answered) => {
|
|
363
316
|
if (site.shorthand !== undefined) {
|
|
@@ -383,14 +336,18 @@ export function references(project, held, targetRoot, options) {
|
|
|
383
336
|
if (test) {
|
|
384
337
|
tests.push(path);
|
|
385
338
|
}
|
|
386
|
-
const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
339
|
+
const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
|
|
387
340
|
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
388
|
-
// the uses
|
|
389
|
-
//
|
|
390
|
-
//
|
|
341
|
+
// the uses and the evaluated modules' specifiers last, each node once: a bare star
|
|
342
|
+
// re-export's specifier is a link and an evaluation both. A shorthand is left out
|
|
343
|
+
// of the batch: its name resolves to the property the literal declares, so the
|
|
344
|
+
// batch's answer for it would name something this project's inventory never holds.
|
|
391
345
|
const batching = [
|
|
392
|
-
...
|
|
393
|
-
|
|
346
|
+
...new Set([
|
|
347
|
+
...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
|
|
348
|
+
...links.map((link) => link.node),
|
|
349
|
+
...evaluations.map((evaluation) => evaluation.node),
|
|
350
|
+
]),
|
|
394
351
|
];
|
|
395
352
|
const answered = new Map();
|
|
396
353
|
batched += batching.length;
|
|
@@ -422,6 +379,25 @@ export function references(project, held, targetRoot, options) {
|
|
|
422
379
|
});
|
|
423
380
|
}
|
|
424
381
|
}
|
|
382
|
+
for (const evaluation of evaluations) {
|
|
383
|
+
const module = answered.get(evaluation.node);
|
|
384
|
+
if (module === undefined) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const position = renderPosition(file, targetRoot, evaluation.node.getStart());
|
|
388
|
+
for (const id of chains.declarationsOf(module)) {
|
|
389
|
+
if (fileIds.has(id)) {
|
|
390
|
+
found.push({
|
|
391
|
+
from: evaluation.from,
|
|
392
|
+
to: id,
|
|
393
|
+
position,
|
|
394
|
+
use: "evaluation",
|
|
395
|
+
resolution: "batch",
|
|
396
|
+
test,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
425
401
|
for (const site of uses) {
|
|
426
402
|
const direct = site.shorthand !== undefined
|
|
427
403
|
? "shorthand"
|
|
@@ -433,7 +409,9 @@ export function references(project, held, targetRoot, options) {
|
|
|
433
409
|
continue;
|
|
434
410
|
}
|
|
435
411
|
const position = renderPosition(file, targetRoot, site.node.getStart());
|
|
436
|
-
|
|
412
|
+
// A re-export something imports is used and so is the declaration behind it, so
|
|
413
|
+
// the use names every link of the chain its name stands at the head of.
|
|
414
|
+
for (const target of chains.chainOf(symbol)) {
|
|
437
415
|
found.push({
|
|
438
416
|
from: site.from,
|
|
439
417
|
to: target.id,
|
|
@@ -456,6 +434,6 @@ export function references(project, held, targetRoot, options) {
|
|
|
456
434
|
.map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
|
|
457
435
|
.sort((a, b) => compare(a.rule, b.rule)),
|
|
458
436
|
testFilePaths: tests.sort(compare),
|
|
459
|
-
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
|
|
437
|
+
cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
|
|
460
438
|
};
|
|
461
439
|
}
|
package/dist/src/roots.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* The root set of one project: every declaration the analysis keeps live without a
|
|
3
3
|
* reference, and why.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* something outside the program reaches is live under
|
|
9
|
-
* candidate under
|
|
5
|
+
* Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
|
|
6
|
+
* that names a caller is live under reference counting as well, which the sweep
|
|
7
|
+
* decides; a root that only supposes a consumer is not, so an exported declaration
|
|
8
|
+
* something outside the program reaches is live under reachability and still a
|
|
9
|
+
* candidate under reference counting.
|
|
10
10
|
*/
|
|
11
|
+
import { aliasChains } from "./alias-chain.js";
|
|
11
12
|
import { REPOSITORY_DOCUMENT } from "./config.js";
|
|
12
13
|
import { entryPoints } from "./entry-points.js";
|
|
13
14
|
import { globExpression } from "./glob.js";
|
|
@@ -143,7 +144,10 @@ export function roots(project, held, targetRoot, options) {
|
|
|
143
144
|
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
144
145
|
enter(point.file, point.rule, point.source, point.exports);
|
|
145
146
|
}
|
|
146
|
-
const exportsOf = exportTables(project, held, [
|
|
147
|
+
const exportsOf = exportTables(project, held, [
|
|
148
|
+
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
149
|
+
...published,
|
|
150
|
+
]);
|
|
147
151
|
for (const entry of entered) {
|
|
148
152
|
const id = fileIds.get(entry.file.fileName);
|
|
149
153
|
if (id === undefined) {
|
|
@@ -181,41 +185,14 @@ export function roots(project, held, targetRoot, options) {
|
|
|
181
185
|
* its export table, star re-exports resolved into it. A file that is no module
|
|
182
186
|
* exports nothing.
|
|
183
187
|
*/
|
|
184
|
-
function exportTables(project, held, wanted
|
|
188
|
+
function exportTables(project, held, wanted) {
|
|
185
189
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
186
190
|
const tables = new Map();
|
|
187
191
|
if (unique.length === 0) {
|
|
188
192
|
return tables;
|
|
189
193
|
}
|
|
190
194
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
191
|
-
const chains =
|
|
192
|
-
const chainOf = (symbol) => {
|
|
193
|
-
const cached = chains.get(symbol.id);
|
|
194
|
-
if (cached !== undefined) {
|
|
195
|
-
return cached;
|
|
196
|
-
}
|
|
197
|
-
const ids = [];
|
|
198
|
-
const walked = new Set();
|
|
199
|
-
let at = symbol;
|
|
200
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
201
|
-
walked.add(at.id);
|
|
202
|
-
for (const handle of at.declarations) {
|
|
203
|
-
if (!ownFiles.has(handle.path)) {
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const node = project.declarationAt(handle)?.node;
|
|
207
|
-
const id = node === undefined
|
|
208
|
-
? undefined
|
|
209
|
-
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
210
|
-
if (id !== undefined && !ids.includes(id)) {
|
|
211
|
-
ids.push(id);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
at = project.aliasStepOf(at);
|
|
215
|
-
}
|
|
216
|
-
chains.set(symbol.id, ids);
|
|
217
|
-
return ids;
|
|
218
|
-
};
|
|
195
|
+
const chains = aliasChains(project, held);
|
|
219
196
|
unique.forEach((file, index) => {
|
|
220
197
|
const module = modules[index];
|
|
221
198
|
if (module === undefined) {
|
|
@@ -224,8 +201,8 @@ function exportTables(project, held, wanted, ownFiles) {
|
|
|
224
201
|
}
|
|
225
202
|
const ids = new Set();
|
|
226
203
|
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
227
|
-
for (const
|
|
228
|
-
ids.add(id);
|
|
204
|
+
for (const target of chains.chainOf(exported)) {
|
|
205
|
+
ids.add(target.id);
|
|
229
206
|
}
|
|
230
207
|
}
|
|
231
208
|
tables.set(file.fileName, [...ids]);
|
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) {
|
package/dist/src/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
2
|
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
|
+
import { DiscoveryError } from "./discover.js";
|
|
3
4
|
/**
|
|
4
5
|
* Opens the compiler client the analysis reads through.
|
|
5
6
|
*
|
|
@@ -81,19 +82,29 @@ export function diagnosticsOf(project) {
|
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* Runs `visit` over every project the compiler configurations name, inside one
|
|
84
|
-
* snapshot of one client.
|
|
85
|
+
* snapshot of one client, in the order they are named, and answers in that order.
|
|
85
86
|
*
|
|
86
87
|
* The snapshot is opened once with every configuration, so each becomes a project
|
|
87
88
|
* with its own program and checker; it is never updated, because the analysis
|
|
88
|
-
* reads and the tree does not change under it.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* reads and the tree does not change under it. The order the snapshot lists its
|
|
90
|
+
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
91
|
+
* instead, and a configuration the snapshot opened no project for ends the run: the
|
|
92
|
+
* run would otherwise answer for fewer projects than it was asked to analyze. The
|
|
93
|
+
* snapshot is disposed once when the last project has been visited, and the client
|
|
94
|
+
* is released after it, whether the visit completed or threw.
|
|
91
95
|
*/
|
|
92
96
|
export function runSession(engine, configFiles, visit) {
|
|
93
97
|
try {
|
|
94
98
|
const snapshot = engine.updateSnapshot(configFiles);
|
|
95
99
|
try {
|
|
96
|
-
const
|
|
100
|
+
const opened = new Map(snapshot.getProjects().map((project) => [project.configFileName, project]));
|
|
101
|
+
const projects = configFiles.map((configFile) => {
|
|
102
|
+
const project = opened.get(configFile);
|
|
103
|
+
if (project === undefined) {
|
|
104
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
105
|
+
}
|
|
106
|
+
return visit(viewOf(project));
|
|
107
|
+
});
|
|
97
108
|
return { projects, timing: engine.getTimingInfo() };
|
|
98
109
|
}
|
|
99
110
|
finally {
|
package/dist/src/sweep.js
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which declarations of one graph are dead, under which liveness relation
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Which declarations of one graph are dead, and under which liveness relation.
|
|
3
|
+
* Reference counting holds a declaration live when any declaration references it,
|
|
4
|
+
* whatever that one's own liveness, or when it is a root of a kind that names a caller;
|
|
5
5
|
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
6
|
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
7
|
*/
|
|
8
|
-
import { componentsOf } from "./components.js";
|
|
9
8
|
import { namesACaller, OUTSIDE } from "./graph.js";
|
|
10
9
|
/**
|
|
11
10
|
* Answers which declarations of one graph are dead under the input's mode.
|
|
12
11
|
*
|
|
13
12
|
* The order is the order the answers depend on: the callers and the marks are live
|
|
14
13
|
* before either relation runs, the candidate set is every judged declaration at least
|
|
15
|
-
* one relation does not hold live
|
|
16
|
-
*
|
|
14
|
+
* one relation does not hold live and no exemption holds back, and the tests of dead
|
|
15
|
+
* code join it.
|
|
17
16
|
*/
|
|
18
17
|
export function sweep(graph, input) {
|
|
19
|
-
const marked = graph.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
// Every root and every mark seeds reachability. A root of a kind that names a caller,
|
|
27
|
-
// and a mark, are live under reference counting as well; a root that only supposes a
|
|
28
|
-
// caller is not.
|
|
18
|
+
const marked = flagged(graph, input.marked);
|
|
19
|
+
const exempt = flagged(graph, (input.exempt ?? []).map((record) => record.id));
|
|
20
|
+
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
21
|
+
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
22
|
+
// names a caller, and a mark, are live under reference counting as well; a root that
|
|
23
|
+
// only supposes a caller is not, and neither is an exemption.
|
|
29
24
|
const called = graph.symbols.map(() => false);
|
|
30
25
|
for (const root of graph.rooted) {
|
|
31
26
|
if (namesACaller(root.kind)) {
|
|
@@ -39,7 +34,7 @@ export function sweep(graph, input) {
|
|
|
39
34
|
}
|
|
40
35
|
return input.mode.production ? made.production : made.production + made.test;
|
|
41
36
|
};
|
|
42
|
-
const reached = reachable(graph,
|
|
37
|
+
const reached = reachable(graph, held, input.mode);
|
|
43
38
|
const liveUnder = graph.symbols.map((_symbol, at) => {
|
|
44
39
|
const relations = [];
|
|
45
40
|
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
@@ -50,8 +45,8 @@ export function sweep(graph, input) {
|
|
|
50
45
|
}
|
|
51
46
|
return relations;
|
|
52
47
|
});
|
|
53
|
-
const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2);
|
|
54
|
-
const testOfDeadCode = admitTestsOfDeadCode(graph, dead,
|
|
48
|
+
const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2);
|
|
49
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, held);
|
|
55
50
|
const candidates = [];
|
|
56
51
|
const live = new Map();
|
|
57
52
|
graph.symbols.forEach((symbol, at) => {
|
|
@@ -70,31 +65,41 @@ export function sweep(graph, input) {
|
|
|
70
65
|
testOfDeadCode: testOfDeadCode[at] === true,
|
|
71
66
|
});
|
|
72
67
|
});
|
|
73
|
-
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
return { liveUnder: live, candidates };
|
|
69
|
+
}
|
|
70
|
+
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
71
|
+
function flagged(graph, ids) {
|
|
72
|
+
const flags = graph.symbols.map(() => false);
|
|
73
|
+
for (const id of ids) {
|
|
74
|
+
const at = graph.at(id);
|
|
75
|
+
if (at !== OUTSIDE) {
|
|
76
|
+
flags[at] = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return flags;
|
|
78
80
|
}
|
|
79
81
|
/**
|
|
80
82
|
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
81
|
-
* reference sets: a
|
|
82
|
-
* no mode withholds a reference it makes,
|
|
83
|
-
*
|
|
83
|
+
* reference sets: a held declaration, marked or exempt, is live by a mechanism the
|
|
84
|
+
* analysis cannot see, so no mode withholds a reference it makes, a test declaration's
|
|
85
|
+
* included, while a root reaches through the references the mode counts. The held seed
|
|
86
|
+
* runs first, so what it reaches is expanded under the wider rule.
|
|
84
87
|
*/
|
|
85
|
-
function reachable(graph,
|
|
88
|
+
function reachable(graph, held, mode) {
|
|
86
89
|
const reached = graph.symbols.map(() => false);
|
|
87
90
|
const expanded = graph.symbols.map(() => false);
|
|
88
91
|
const queue = [];
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
92
|
+
// Reaching a file reaches its top level: the references written outside every
|
|
93
|
+
// declaration it holds. A reference that reads a file reads the module's namespace,
|
|
94
|
+
// which reaches what the module exports as well. An evaluation runs the top level
|
|
95
|
+
// and reads no export, and a root does not expand either: a file a runtime runs
|
|
96
|
+
// without reading its exports is a root by itself.
|
|
97
|
+
const enter = (at, expands) => {
|
|
93
98
|
if (!reached[at]) {
|
|
94
99
|
reached[at] = true;
|
|
95
100
|
queue.push(at);
|
|
96
101
|
}
|
|
97
|
-
if (!
|
|
102
|
+
if (!expands || expanded[at] === true) {
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
100
105
|
expanded[at] = true;
|
|
@@ -102,18 +107,18 @@ function reachable(graph, marked, mode) {
|
|
|
102
107
|
enter(exported, false);
|
|
103
108
|
}
|
|
104
109
|
};
|
|
105
|
-
const walk = (
|
|
110
|
+
const walk = (holding) => {
|
|
106
111
|
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
107
112
|
for (const edge of graph.out[at] ?? []) {
|
|
108
|
-
if (!
|
|
113
|
+
if (!holding && mode.production && edge.test) {
|
|
109
114
|
continue;
|
|
110
115
|
}
|
|
111
|
-
enter(edge.to,
|
|
116
|
+
enter(edge.to, !edge.evaluation);
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
};
|
|
115
|
-
|
|
116
|
-
if (
|
|
120
|
+
held.forEach((hold, at) => {
|
|
121
|
+
if (hold) {
|
|
117
122
|
enter(at, false);
|
|
118
123
|
}
|
|
119
124
|
});
|
|
@@ -128,12 +133,13 @@ function reachable(graph, marked, mode) {
|
|
|
128
133
|
* Admits every test declaration whose set of referenced production declarations is
|
|
129
134
|
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
130
135
|
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
131
|
-
* which is what makes the targets dead. A test referencing one live target is never
|
|
136
|
+
* which is what makes the targets dead. A test referencing one live target is never
|
|
137
|
+
* admitted, and neither is a marked or exempt test.
|
|
132
138
|
*/
|
|
133
|
-
function admitTestsOfDeadCode(graph, dead,
|
|
139
|
+
function admitTestsOfDeadCode(graph, dead, held) {
|
|
134
140
|
const admitted = graph.symbols.map(() => false);
|
|
135
141
|
graph.symbols.forEach((_symbol, at) => {
|
|
136
|
-
if (graph.test[at] !== true || graph.subject[at] !== true ||
|
|
142
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
145
|
let targets = 0;
|
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.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/js": "10.0.1",
|
|
35
|
-
"@types/node": "24.
|
|
35
|
+
"@types/node": "24.19.0",
|
|
36
36
|
"eslint": "10.11.0",
|
|
37
|
-
"fast-check": "4.10.
|
|
38
|
-
"prettier": "3.9.
|
|
37
|
+
"fast-check": "4.10.2",
|
|
38
|
+
"prettier": "3.9.9",
|
|
39
39
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
40
|
-
"typescript-eslint": "8.70.
|
|
41
|
-
"vitest": "5.0.
|
|
40
|
+
"typescript-eslint": "8.70.1",
|
|
41
|
+
"vitest": "5.0.2"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|