@cplieger/deadset-ts 1.2.0 → 2.0.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 = "1.7.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.0.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.0.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
@@ -133,6 +133,17 @@ export interface Inventory {
133
133
  /** The compiler configuration the project was opened from. */
134
134
  readonly configFile: string;
135
135
  readonly symbols: readonly InventorySymbol[];
136
+ /**
137
+ * Each declaring node's {@link nodeKey} mapped to the identifier of the declaration
138
+ * enumerated there. A reading that starts from a node of the same program, rather
139
+ * than from a position, resolves the declaration through this map instead of
140
+ * rendering the position a second time, so the rule that decides a declaration's
141
+ * identifier has one owner.
142
+ *
143
+ * A getter and a setter of one name are one declaration and two nodes, so both nodes
144
+ * name it.
145
+ */
146
+ readonly declarations: ReadonlyMap<string, string>;
136
147
  readonly cost: InventoryCost;
137
148
  /**
138
149
  * The module specifiers of the bare star re-exports the project's files carry, in
@@ -407,8 +418,15 @@ function nameNodeOf(node: Node): Node | undefined {
407
418
  return (node as { readonly name?: Node }).name;
408
419
  }
409
420
 
410
- /** One node's identity across the two mechanisms, which resolve it separately. */
411
- function nodeKey(file: SourceFile, node: Node): string {
421
+ /**
422
+ * One node's identity inside one program, which is how two readings of that program
423
+ * name one declaration.
424
+ *
425
+ * The two mechanisms behind the member set resolve a member separately and meet here;
426
+ * so does the reference pass, which starts from a resolved symbol's declaration and
427
+ * needs the declaration this enumeration made for it.
428
+ */
429
+ export function nodeKey(file: SourceFile, node: Node): string {
412
430
  return `${file.fileName}:${String(node.pos)}:${String(node.end)}`;
413
431
  }
414
432
 
@@ -828,8 +846,10 @@ export function inventory<Brand>(
828
846
  const endLine = renderPosition(file, targetRoot, Math.max(node.end - 1, 0)).line;
829
847
  if (held !== undefined) {
830
848
  // 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.
849
+ // second declaration widens the first record rather than making a second. Both
850
+ // nodes name that record, so a reading that starts from either one finds it.
832
851
  held.endLine = Math.max(held.endLine, endLine);
852
+ byNode.set(nodeKey(file, node), held);
833
853
  return;
834
854
  }
835
855
  const at = name ?? node;
@@ -884,6 +904,9 @@ export function inventory<Brand>(
884
904
  return {
885
905
  configFile: project.configFile,
886
906
  symbols,
907
+ declarations: new Map(
908
+ [...byNode].map(([key, record]) => [key, positionKey(record.position)] as const),
909
+ ),
887
910
  cost: { batches: 1, exportTables, memberTables },
888
911
  starReExports,
889
912
  outsideOwnFiles,