@cplieger/deadset-ts 5.1.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The overrides of the target's class members. A call through the base class, `this`
3
+ * in the base's own body or a value typed as the base, names the base's member and
4
+ * runs the override of whichever subclass the value is, so the base member's
5
+ * declaration references every override as a use of it.
6
+ */
7
+ import { isClassDeclaration, isClassExpression, isExpressionWithTypeArguments, isPropertyAccessExpression, isQualifiedName, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { memberComponent } from "./class-members.js";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { UNANSWERED } from "./query.js";
11
+ /** The member kinds a subclass overrides. */
12
+ const OVERRIDABLE = new Set(["method", "class-member"]);
13
+ /** The node naming the class one `extends` entry names. */
14
+ function nameOf(entry) {
15
+ if (isExpressionWithTypeArguments(entry)) {
16
+ return isPropertyAccessExpression(entry.expression) ? entry.expression.name : entry.expression;
17
+ }
18
+ return isQualifiedName(entry.typeName) ? entry.typeName.right : entry.typeName;
19
+ }
20
+ /** Each class of the target whose `extends` clause names a class, with the name node. */
21
+ function extending(project, held) {
22
+ const found = [];
23
+ for (const file of project.ownSourceFiles()) {
24
+ const visit = (node) => {
25
+ if (isClassDeclaration(node) || isClassExpression(node)) {
26
+ const id = held.declarations.get(nodeKey(file, node));
27
+ const clause = node.heritageClauses?.find((one) => one.token === SyntaxKind.ExtendsKeyword);
28
+ const base = clause?.types[0];
29
+ if (id !== undefined && base !== undefined) {
30
+ found.push({ id, base: nameOf(base) });
31
+ }
32
+ }
33
+ node.forEachChild(visit);
34
+ };
35
+ file.forEachChild(visit);
36
+ }
37
+ return found;
38
+ }
39
+ /**
40
+ * The references each base class member makes to its overrides, for the classes of one
41
+ * project that extend another class of the target. A member a nearer base already
42
+ * overrides is reached through that override. A private member is not overridden.
43
+ * Each reference is placed at the override and made by its file.
44
+ */
45
+ export function overrideReferences(project, held, chains, testFiles) {
46
+ const classes = extending(project, held);
47
+ if (classes.length === 0) {
48
+ return [];
49
+ }
50
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
51
+ const members = new Map();
52
+ for (const symbol of held.symbols) {
53
+ const component = memberComponent(symbol, byId);
54
+ if (component === undefined ||
55
+ !OVERRIDABLE.has(symbol.kind) ||
56
+ symbol.visibility === "private" ||
57
+ symbol.visibility === "private-name") {
58
+ continue;
59
+ }
60
+ const own = members.get(symbol.parent) ?? new Map();
61
+ own.set(component, symbol);
62
+ members.set(symbol.parent, own);
63
+ }
64
+ const bases = new Map();
65
+ const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
66
+ classes.forEach(({ id }, index) => {
67
+ const symbol = symbols[index];
68
+ if (symbol !== undefined && symbol !== UNANSWERED) {
69
+ bases.set(id, chains
70
+ .chainOf(symbol)
71
+ .map((target) => target.id)
72
+ .filter((target) => byId.get(target)?.kind === "class" && target !== id));
73
+ }
74
+ });
75
+ /** The nearest member of one component the class or a class it extends declares. */
76
+ const declaredAbove = (classId, component, seen) => {
77
+ if (seen.has(classId)) {
78
+ return [];
79
+ }
80
+ seen.add(classId);
81
+ const own = members.get(classId)?.get(component);
82
+ if (own !== undefined) {
83
+ return [own.id];
84
+ }
85
+ return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
86
+ };
87
+ const found = [];
88
+ for (const [classId, baseIds] of bases) {
89
+ for (const [component, member] of members.get(classId) ?? []) {
90
+ const seen = new Set([classId]);
91
+ for (const base of baseIds.flatMap((one) => declaredAbove(one, component, seen))) {
92
+ found.push({
93
+ from: base,
94
+ to: member.id,
95
+ position: member.position,
96
+ use: "read",
97
+ resolution: "override",
98
+ test: testFiles.has(member.position.path),
99
+ });
100
+ }
101
+ }
102
+ }
103
+ return found;
104
+ }
package/dist/src/paths.js CHANGED
@@ -65,6 +65,23 @@ export function dirnamePath(path) {
65
65
  export function resolvePath(against, path) {
66
66
  return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
67
67
  }
68
+ /**
69
+ * The relative path before the query of a relative module specifier such as
70
+ * `./worker.ts?worker`, the module a bundler loads; undefined for a specifier with no
71
+ * query, one that is not relative, and one whose query has a `raw` parameter, which
72
+ * reads the file as text and so evaluates nothing.
73
+ */
74
+ export function queriedModulePath(specifier) {
75
+ const query = specifier.indexOf("?");
76
+ if (query < 0 || !(specifier.startsWith("./") || specifier.startsWith("../"))) {
77
+ return undefined;
78
+ }
79
+ const raw = specifier
80
+ .slice(query + 1)
81
+ .split("&")
82
+ .some((parameter) => parameter.split("=")[0] === "raw");
83
+ return raw ? undefined : specifier.slice(0, query);
84
+ }
68
85
  /**
69
86
  * One path as it reads below a root, or undefined when it is not below the root.
70
87
  * Both are normalized first, so a root written with a trailing separator or a `.`
@@ -4,7 +4,8 @@ import { contextualUses } from "./contextual-uses.js";
4
4
  import { destructuring, propertyReadsOf } from "./destructuring.js";
5
5
  import { globExpression } from "./glob.js";
6
6
  import { declarationsByName, nodeKey } from "./inventory.js";
7
- import { relativePath } from "./paths.js";
7
+ import { overrideReferences } from "./overrides.js";
8
+ import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.js";
8
9
  import { byPosition, isComponentFile, renderPosition } from "./position.js";
9
10
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
10
11
  import { typeQueryAliases } from "./type-query-alias.js";
@@ -200,6 +201,19 @@ function evaluatedBy(node) {
200
201
  }
201
202
  return undefined;
202
203
  }
204
+ /**
205
+ * The own file a module specifier with a query evaluates ({@link queriedModulePath}): the
206
+ * checker resolves the whole specifier to whatever module declaration matches it.
207
+ */
208
+ function queriedFile(file, specifier, fileIdByName) {
209
+ if (!isStringLiteral(specifier) && !isNoSubstitutionTemplateLiteral(specifier)) {
210
+ return undefined;
211
+ }
212
+ const path = queriedModulePath(specifier.text);
213
+ return path === undefined
214
+ ? undefined
215
+ : fileIdByName.get(resolvePath(dirnamePath(file.fileName), path));
216
+ }
203
217
  /**
204
218
  * The re-exports one node writes, if it is an export declaration. A specifier and a
205
219
  * namespace export are declarations whose names are where they are written, so no use is
@@ -654,6 +668,7 @@ export function references(project, held, targetRoot, options) {
654
668
  const fileId = declaredAt(file) ?? "";
655
669
  return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
656
670
  });
671
+ const fileIdByName = new Map(walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]));
657
672
  const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
658
673
  const writtenBy = new Map();
659
674
  for (const write of contextual.writes) {
@@ -796,6 +811,17 @@ export function references(project, held, targetRoot, options) {
796
811
  }
797
812
  }
798
813
  for (const evaluation of evaluations) {
814
+ const queried = queriedFile(file, evaluation.node, fileIdByName);
815
+ if (queried !== undefined) {
816
+ found.push({
817
+ from: evaluation.from,
818
+ to: queried,
819
+ position: renderPosition(file, root, evaluation.node.getStart()),
820
+ use: "evaluation",
821
+ resolution: "syntax",
822
+ test,
823
+ });
824
+ }
799
825
  const module = answered.get(evaluation.node);
800
826
  if (module === undefined) {
801
827
  continue;
@@ -924,6 +950,9 @@ export function references(project, held, targetRoot, options) {
924
950
  }
925
951
  }
926
952
  }
953
+ if (consumer === undefined) {
954
+ found.push(...overrideReferences(project, held, chains, new Set(tests)));
955
+ }
927
956
  found.sort((a, b) => byPosition(a.position, b.position) ||
928
957
  compare(a.to, b.to) ||
929
958
  compare(a.from, b.from) ||
package/dist/src/roots.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * decides; the published API only supposes a consumer, so a published export is live
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
+ import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
10
11
  import { aliasChains } from "./alias-chain.js";
11
12
  import { conventionEntries } from "./conventions.js";
12
13
  import { entryPoints } from "./entry-points.js";
@@ -131,7 +132,7 @@ export function roots(project, held, targetRoot, options) {
131
132
  publishes: entry.published &&
132
133
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
133
134
  })),
134
- ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
+ ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
136
  ];
136
137
  for (const { entry, publishes } of entries) {
137
138
  if (options.publishedAPI && publishes) {
@@ -188,10 +189,11 @@ export function roots(project, held, targetRoot, options) {
188
189
  }
189
190
  }
190
191
  }
191
- if (options.publishedAPI) {
192
- for (const id of publishedAPI(published, exportsOf, held)) {
193
- add(id, "published-api", "");
194
- }
192
+ const api = options.publishedAPI
193
+ ? publishedAPI(project, published, exportsOf, held, files.byPath)
194
+ : { ids: [], guessed: [] };
195
+ for (const id of api.ids) {
196
+ add(id, "published-api", "");
195
197
  }
196
198
  const unmatched = configured(held, options.patterns, add);
197
199
  const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
@@ -205,7 +207,7 @@ export function roots(project, held, targetRoot, options) {
205
207
  configFile: project.configFile,
206
208
  liveUnderReachability: ordered,
207
209
  unmatched,
208
- unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
210
+ unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
209
211
  };
210
212
  }
211
213
  /**
@@ -255,52 +257,134 @@ function exportTables(project, held, wanted) {
255
257
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
256
258
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
257
259
  /**
258
- * Every declaration a consumer of the library can name: what the published files
259
- * export, and below each, the members a consumer reaches through it.
260
- *
261
- * The published files are the ones `exports` reaches where the manifest declares
262
- * one, because a package that declares `exports` has said exactly what a consumer may
263
- * import; where it declares none, they are the ones the members that preceded it
264
- * name. Below a published module or namespace the walk follows what its export table
265
- * names; below any other published declaration it follows every member a consumer
266
- * can write: a public one, and a protected one, which a consumer's subclass names. A
267
- * private member is not published, nor is anything below one, and a type parameter
268
- * is not published either, because a consumer supplies a type argument by position.
269
- *
270
- * A published file is not itself published. A consumer imports a module and names
271
- * what it exports; the file is a root because the manifest names it, which is a
272
- * different reason.
260
+ * Every declaration a consumer of the library can name: what the published files export
261
+ * and, below each, the members a consumer writes: a namespace's exported ones, any other
262
+ * declaration's public and protected ones, never a private one or a type parameter. A
263
+ * declaration a published one's type names is published too, exported or not. A type name
264
+ * the checker leaves unanswered publishes every declaration spelled as it is, and those are
265
+ * `guessed`. A published file is not itself published.
273
266
  */
274
- function publishedAPI(published, exportsOf, held) {
267
+ function publishedAPI(project, published, exportsOf, held, byPath) {
275
268
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
276
269
  const children = childrenOf(held);
270
+ const nodes = declarationNodes(held, byPath);
271
+ const chains = aliasChains(project, held);
277
272
  const reached = new Set();
273
+ const guessed = new Set();
278
274
  const pending = [];
275
+ const reach = (id) => {
276
+ if (!reached.has(id) && byId.get(id)?.kind !== "file") {
277
+ reached.add(id);
278
+ pending.push(id);
279
+ }
280
+ };
279
281
  for (const file of published) {
280
282
  for (const id of exportsOf.get(file.fileName) ?? []) {
281
283
  if (byId.get(id)?.kind === "file") {
282
284
  pending.push(id);
283
285
  }
284
- else if (!reached.has(id)) {
285
- reached.add(id);
286
- pending.push(id);
286
+ else {
287
+ reach(id);
287
288
  }
288
289
  }
289
290
  }
290
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
291
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
292
- for (const member of children.get(next) ?? []) {
293
- const reachable = exporting
294
- ? member.exported
295
- : member.visibility !== "private" && member.visibility !== "private-name";
296
- if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
297
- continue;
291
+ while (pending.length > 0) {
292
+ const round = [];
293
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
294
+ round.push(next);
295
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
296
+ for (const member of children.get(next) ?? []) {
297
+ const reachable = exporting
298
+ ? member.exported
299
+ : member.visibility !== "private" && member.visibility !== "private-name";
300
+ if (member.kind !== "type-parameter" && reachable) {
301
+ reach(member.id);
302
+ }
298
303
  }
299
- reached.add(member.id);
300
- pending.push(member.id);
301
304
  }
305
+ const named = round.flatMap((id) => {
306
+ const symbol = byId.get(id);
307
+ return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
308
+ });
309
+ project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
310
+ if (symbol === UNANSWERED) {
311
+ for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
312
+ guessed.add(id);
313
+ reach(id);
314
+ }
315
+ }
316
+ else if (symbol !== undefined) {
317
+ for (const target of chains.chainOf(symbol)) {
318
+ reach(target.id);
319
+ }
320
+ }
321
+ });
302
322
  }
303
- return [...reached];
323
+ return { ids: [...reached], guessed: [...guessed] };
324
+ }
325
+ /** The type names declarations of the inventory write, each file walked once, when first asked. */
326
+ function declarationNodes(held, byPath) {
327
+ const walked = new Map();
328
+ const nodesIn = (path) => {
329
+ const known = walked.get(path);
330
+ if (known !== undefined) {
331
+ return known;
332
+ }
333
+ const found = { byId: new Map(), nodes: new Set() };
334
+ const file = byPath.get(path);
335
+ if (file !== undefined) {
336
+ const visit = (node) => {
337
+ const id = held.declarations.get(nodeKey(file, node));
338
+ if (id !== undefined) {
339
+ found.nodes.add(node);
340
+ if (!found.byId.has(id)) {
341
+ found.byId.set(id, node);
342
+ }
343
+ }
344
+ node.forEachChild(visit);
345
+ };
346
+ visit(file);
347
+ }
348
+ walked.set(path, found);
349
+ return found;
350
+ };
351
+ return {
352
+ typeNamesOf: (symbol) => {
353
+ const { byId, nodes } = nodesIn(symbol.position.path);
354
+ const declaration = byId.get(symbol.id);
355
+ return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
356
+ },
357
+ };
358
+ }
359
+ /**
360
+ * The names of types one declaration's own syntax writes: in a type reference, a
361
+ * heritage clause or a type query. A function body is not read, and neither is a
362
+ * declaration nested in this one, which `nested` holds and which is read on its own.
363
+ */
364
+ function typeNamesOf(declaration, nested) {
365
+ const found = [];
366
+ const visit = (node) => {
367
+ if (node !== declaration && nested.has(node)) {
368
+ return;
369
+ }
370
+ if (isTypeReferenceNode(node)) {
371
+ found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
372
+ }
373
+ else if (isExpressionWithTypeArguments(node)) {
374
+ found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
375
+ }
376
+ else if (isTypeQueryNode(node)) {
377
+ found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
378
+ }
379
+ const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
380
+ node.forEachChild((child) => {
381
+ if (child !== body) {
382
+ visit(child);
383
+ }
384
+ });
385
+ };
386
+ visit(declaration);
387
+ return found;
304
388
  }
305
389
  /**
306
390
  * Keeps every declaration the configuration names and returns every string that
@@ -225,10 +225,10 @@ function projectsOf(snapshot, built, written) {
225
225
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
226
226
  * built as that program, or opened from the configuration it writes, instead of as a project.
227
227
  * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
228
- * under `"record"`. The snapshot is disposed after the last visit and the client released
229
- * after it, whether the visit completed or threw.
228
+ * under `"record"`. `last` runs after the last visit while the client still answers; the
229
+ * snapshot is disposed and the client released after it, whether the visit completed or threw.
230
230
  */
231
- export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
231
+ export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
232
232
  try {
233
233
  const built = [];
234
234
  const params = [];
@@ -262,6 +262,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
262
262
  }
263
263
  projects.push(visit(viewOf(project, configFile, engine, log)));
264
264
  }
265
+ last();
265
266
  return {
266
267
  projects,
267
268
  unopened: missing,
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
87
87
  }
88
88
  return at + 1;
89
89
  }
90
- /** The identifiers one action's inner text writes outside a quoted string, with their offsets in it. */
91
- function identifiersOf(inner) {
90
+ /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
91
+ function substitutionEnd(text, from) {
92
+ let depth = 1;
93
+ let at = from;
94
+ while (at < text.length) {
95
+ const char = text[at];
96
+ if (char === '"' || char === "'") {
97
+ at = stringEnd(text, at);
98
+ continue;
99
+ }
100
+ if (char === "`") {
101
+ at = templateEnd(text, at, () => undefined);
102
+ continue;
103
+ }
104
+ if (char === "{") {
105
+ depth += 1;
106
+ }
107
+ else if (char === "}") {
108
+ depth -= 1;
109
+ if (depth === 0) {
110
+ return at + 1;
111
+ }
112
+ }
113
+ at += 1;
114
+ }
115
+ return at;
116
+ }
117
+ /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
118
+ function templateEnd(text, from, substitution) {
119
+ let at = from + 1;
120
+ while (at < text.length && text[at] !== "`") {
121
+ if (text[at] === "\\") {
122
+ at += 2;
123
+ continue;
124
+ }
125
+ if (text[at] === "$" && text[at + 1] === "{") {
126
+ const start = at + 2;
127
+ const end = substitutionEnd(text, start);
128
+ substitution(text.slice(start, end - 1), start);
129
+ at = end;
130
+ continue;
131
+ }
132
+ at += 1;
133
+ }
134
+ return at + 1;
135
+ }
136
+ /**
137
+ * The identifiers one action's inner text writes outside a quoted string, with their offsets
138
+ * in it. In `script` text, a component's markup, a template literal's substitutions are code.
139
+ */
140
+ function identifiersOf(inner, script = false) {
92
141
  const found = [];
93
142
  let at = 0;
94
143
  while (at < inner.length) {
95
144
  const char = inner[at] ?? "";
145
+ if (char === "`" && script) {
146
+ at = templateEnd(inner, at, (expression, offset) => {
147
+ for (const one of identifiersOf(expression, true)) {
148
+ found.push({ name: one.name, offset: offset + one.offset });
149
+ }
150
+ });
151
+ continue;
152
+ }
96
153
  if (char === '"' || char === "'" || char === "`") {
97
154
  at = stringEnd(inner, at);
98
155
  continue;
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
142
199
  }
143
200
  /** Every identifier the markup of one component file writes in an action. */
144
201
  function markupNamesOf(text) {
145
- return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
202
+ return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
146
203
  name,
147
204
  offset: one.start + offset,
148
205
  action: one.action,
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",