@cplieger/deadset-ts 1.2.0 → 2.1.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.
@@ -0,0 +1,101 @@
1
+ import { API } from "@typescript/native/unstable/sync";
2
+ import { isIdentifier } from "@typescript/native/unstable/ast";
3
+ /**
4
+ * Opens the compiler client the analysis reads through.
5
+ *
6
+ * Closing twice releases once: a run that fails before its snapshot opens closes
7
+ * the client where it failed, and the outer release runs whatever happened, so the
8
+ * second call has nothing left to do.
9
+ */
10
+ export function openEngine(options) {
11
+ const api = new API({ collectTiming: options.collectTiming });
12
+ let closed = false;
13
+ return {
14
+ parseConfigFile: (file) => api.parseConfigFile(file),
15
+ updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
16
+ getTimingInfo: () => api.getTimingInfo(),
17
+ close: () => {
18
+ if (closed) {
19
+ return;
20
+ }
21
+ closed = true;
22
+ api.close();
23
+ },
24
+ };
25
+ }
26
+ function viewOf(project) {
27
+ // The snapshot is never updated, so a project's own file set is the same answer
28
+ // every time it is asked for. It is read once because the question is not free:
29
+ // deciding it consults the metadata of every file the program holds, the compiler's
30
+ // own library files included, and a run asks for the set once per pass.
31
+ let ownFiles;
32
+ return {
33
+ configFile: project.configFileName,
34
+ program: project.program,
35
+ checker: project.checker,
36
+ ownSourceFiles: () => {
37
+ ownFiles ??= project.program
38
+ .getSourceFileNames()
39
+ // The metadata decides which files belong to the target, and it is read by
40
+ // name. Fetching each file first and filtering afterwards costs one round
41
+ // trip per library file the answer then discards, and a program holds far
42
+ // more library files than target files.
43
+ .filter((name) => {
44
+ const metadata = project.program.getSourceFileMetadata(name);
45
+ return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
46
+ })
47
+ .map((name) => project.program.getSourceFile(name))
48
+ .filter((file) => file !== undefined);
49
+ return ownFiles;
50
+ },
51
+ handle: (node) => ({ node }),
52
+ symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
53
+ resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
54
+ shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
55
+ aliasStepOf: (symbol) => {
56
+ const held = project.checker.getImmediateAliasedSymbol(symbol);
57
+ // An alias that resolves to nothing answers with the checker's own unknown
58
+ // symbol, which declares nothing; a symbol that is no alias answers with
59
+ // nothing at all.
60
+ return held === undefined || held.declarations.length === 0 ? undefined : held;
61
+ },
62
+ declarationAt: (handle) => {
63
+ const node = handle.resolve(project);
64
+ return node === undefined ? undefined : { node };
65
+ },
66
+ };
67
+ }
68
+ /** The three diagnostic sets one project carries, read before any analysis. */
69
+ export function diagnosticsOf(project) {
70
+ return {
71
+ configFile: project.configFile,
72
+ syntactic: project.program.getSyntacticDiagnostics(),
73
+ semantic: project.program.getSemanticDiagnostics(),
74
+ configParsing: project.program.getConfigFileParsingDiagnostics(),
75
+ };
76
+ }
77
+ /**
78
+ * Runs `visit` over every project the compiler configurations name, inside one
79
+ * snapshot of one client.
80
+ *
81
+ * The snapshot is opened once with every configuration, so each becomes a project
82
+ * with its own program and checker; it is never updated, because the analysis
83
+ * reads and the tree does not change under it. It is disposed once when the last
84
+ * project has been visited, and the client is released after it, whether the visit
85
+ * completed or threw.
86
+ */
87
+ export function runSession(engine, configFiles, visit) {
88
+ try {
89
+ const snapshot = engine.updateSnapshot(configFiles);
90
+ try {
91
+ const projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
92
+ return { projects, timing: engine.getTimingInfo() };
93
+ }
94
+ finally {
95
+ snapshot.dispose();
96
+ }
97
+ }
98
+ finally {
99
+ engine.close();
100
+ }
101
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The Contract version this analyzer implements. It is the resolved value of
3
+ * `contract_version` when no configuration source supplies one, and it moves
4
+ * independently of the analyzer's own version, which is the one the host answers
5
+ * from the package the analyzer was installed from.
6
+ */
7
+ export const CONTRACT_VERSION = "2.0.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "1.2.0",
3
+ "version": "2.1.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,20 +1,25 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "1.2.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
7
7
  "license": "GPL-3.0-or-later",
8
8
  "bin": {
9
- "deadset-ts": "./bin/deadset-ts.mjs"
9
+ "deadset-ts": "./dist/bin/deadset-ts.js"
10
10
  },
11
11
  "exports": {
12
- ".": "./src/index.ts"
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./dist/src/index.js"
15
+ }
13
16
  },
14
17
  "engines": {
15
18
  "node": ">=24"
16
19
  },
17
20
  "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "prepack": "npm run build",
18
23
  "typecheck": "tsc -p tsconfig.json",
19
24
  "typecheck:tests": "tsc -p tsconfig.test.json",
20
25
  "test": "vitest --run",
@@ -58,7 +63,7 @@
58
63
  "files": [
59
64
  "LICENSE",
60
65
  "NOTICE",
61
- "bin/",
66
+ "dist/",
62
67
  "src/",
63
68
  "!src/**/*.test.ts",
64
69
  "jsr.json"
package/src/host.ts CHANGED
@@ -8,8 +8,9 @@ export interface DirectoryEntry {
8
8
  }
9
9
 
10
10
  /**
11
- * The filesystem the analysis reads, and the directory it reads relative paths
12
- * against.
11
+ * The platform one run reads: the filesystem, the directory relative paths
12
+ * resolve against, and the version of the package this analyzer was installed
13
+ * from.
13
14
  *
14
15
  * It is a parameter rather than an import because a published module that reads
15
16
  * the filesystem directly carries the whole platform's type surface into every
@@ -26,4 +27,11 @@ export interface Host {
26
27
  readDirectory(path: string): readonly DirectoryEntry[];
27
28
  /** What one path names, `"absent"` where nothing is there. */
28
29
  kindOf(path: string): PathKind;
30
+ /**
31
+ * The version of the package this analyzer was installed from, or a throw
32
+ * naming why it cannot be known. It is a method rather than a value because
33
+ * locating that package is a filesystem walk, and a host that cannot do it
34
+ * says so at the one moment the value is asked for.
35
+ */
36
+ analyzerVersion(): string;
29
37
  }
package/src/index.ts CHANGED
@@ -2,12 +2,24 @@ export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
2
2
  export type { DirectoryEntry, Host, PathKind } from "./host.ts";
3
3
  export {
4
4
  inventory,
5
+ nodeKey,
5
6
  type Inventory,
6
7
  type InventoryCost,
7
8
  type InventorySymbol,
8
9
  type SymbolKind,
9
10
  type Visibility,
10
11
  } from "./inventory.ts";
12
+ export {
13
+ DEFAULT_BATCH_CAP,
14
+ references,
15
+ type Reference,
16
+ type ReferenceCost,
17
+ type ReferenceOptions,
18
+ type References,
19
+ type Resolution,
20
+ type TestFileRule,
21
+ type Use,
22
+ } from "./references.ts";
11
23
  export {
12
24
  byPosition,
13
25
  positionKey,
@@ -26,4 +38,4 @@ export {
26
38
  type Fragment,
27
39
  type Module,
28
40
  } from "./ref.ts";
29
- export { CONTRACT_VERSION, version } from "./version.ts";
41
+ export { CONTRACT_VERSION } from "./version.ts";
package/src/inventory.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  isModuleBlock,
16
16
  isModuleDeclaration,
17
17
  isNamespaceExport,
18
+ isParenthesizedTypeNode,
18
19
  isPrivateIdentifier,
19
20
  isPropertyDeclaration,
20
21
  isPropertySignatureDeclaration,
@@ -133,6 +134,17 @@ export interface Inventory {
133
134
  /** The compiler configuration the project was opened from. */
134
135
  readonly configFile: string;
135
136
  readonly symbols: readonly InventorySymbol[];
137
+ /**
138
+ * Each declaring node's {@link nodeKey} mapped to the identifier of the declaration
139
+ * enumerated there. A reading that starts from a node of the same program, rather
140
+ * than from a position, resolves the declaration through this map instead of
141
+ * rendering the position a second time, so the rule that decides a declaration's
142
+ * identifier has one owner.
143
+ *
144
+ * A getter and a setter of one name are one declaration and two nodes, so both nodes
145
+ * name it.
146
+ */
147
+ readonly declarations: ReadonlyMap<string, string>;
136
148
  readonly cost: InventoryCost;
137
149
  /**
138
150
  * The module specifiers of the bare star re-exports the project's files carry, in
@@ -316,6 +328,31 @@ function memberKey(component: Component, isStatic: boolean): string {
316
328
  return `${component.computed ? "[" : ""}${component.text}${isStatic ? ":static" : ""}`;
317
329
  }
318
330
 
331
+ /** The type one node is, through however many parentheses are written around it. */
332
+ function unparenthesizedType(type: Node): Node {
333
+ let held = type;
334
+ while (isParenthesizedTypeNode(held)) {
335
+ held = held.type;
336
+ }
337
+ return held;
338
+ }
339
+
340
+ /**
341
+ * The direct constituents of one type. Parentheses are syntax and are not a
342
+ * constituent, so a parenthesized type contributes the constituents it holds: the
343
+ * constituents of `(A | B) & C` are `A`, `B` and `C`. A union or an intersection
344
+ * written without them stays one constituent.
345
+ */
346
+ function constituentsOf(type: Node): readonly Node[] {
347
+ const held = unparenthesizedType(type);
348
+ if (!isUnionTypeNode(held) && !isIntersectionTypeNode(held)) {
349
+ return [held];
350
+ }
351
+ return held.types.flatMap((member) =>
352
+ isParenthesizedTypeNode(member) ? constituentsOf(member) : [member],
353
+ );
354
+ }
355
+
319
356
  /**
320
357
  * The members one type alias declares: the members of the object type it writes, and
321
358
  * of every object type that is a direct constituent of a union or an intersection it
@@ -327,11 +364,7 @@ function memberKey(component: Component, isStatic: boolean): string {
327
364
  * neither of them is the one a reference to it would name.
328
365
  */
329
366
  function aliasMembers(type: Node): readonly Node[] {
330
- const literals = isTypeLiteralNode(type)
331
- ? [type]
332
- : isUnionTypeNode(type) || isIntersectionTypeNode(type)
333
- ? type.types.filter(isTypeLiteralNode)
334
- : [];
367
+ const literals = constituentsOf(type).filter(isTypeLiteralNode);
335
368
  if (literals.length < 2) {
336
369
  return literals[0]?.members ?? [];
337
370
  }
@@ -407,8 +440,15 @@ function nameNodeOf(node: Node): Node | undefined {
407
440
  return (node as { readonly name?: Node }).name;
408
441
  }
409
442
 
410
- /** One node's identity across the two mechanisms, which resolve it separately. */
411
- function nodeKey(file: SourceFile, node: Node): string {
443
+ /**
444
+ * One node's identity inside one program, which is how two readings of that program
445
+ * name one declaration.
446
+ *
447
+ * The two mechanisms behind the member set resolve a member separately and meet here;
448
+ * so does the reference pass, which starts from a resolved symbol's declaration and
449
+ * needs the declaration this enumeration made for it.
450
+ */
451
+ export function nodeKey(file: SourceFile, node: Node): string {
412
452
  return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
413
453
  }
414
454
 
@@ -828,8 +868,10 @@ export function inventory<Brand>(
828
868
  const endLine = renderPosition(file, targetRoot, Math.max(node.end - 1, 0)).line;
829
869
  if (held !== undefined) {
830
870
  // A getter and a setter of one name are one symbol and one reference, so the
831
- // second declaration widens the first record rather than making a second.
871
+ // second declaration widens the first record rather than making a second. Both
872
+ // nodes name that record, so a reading that starts from either one finds it.
832
873
  held.endLine = Math.max(held.endLine, endLine);
874
+ byNode.set(nodeKey(file, node), held);
833
875
  return;
834
876
  }
835
877
  const at = name ?? node;
@@ -884,6 +926,9 @@ export function inventory<Brand>(
884
926
  return {
885
927
  configFile: project.configFile,
886
928
  symbols,
929
+ declarations: new Map(
930
+ [...byNode].map(([key, record]) => [key, positionKey(record.position)] as const),
931
+ ),
887
932
  cost: { batches: 1, exportTables, memberTables },
888
933
  starReExports,
889
934
  outsideOwnFiles,