@cplieger/deadset-ts 3.3.0 → 3.4.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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +8 -1
  3. package/dist/src/calls.js +154 -0
  4. package/dist/src/class-members.js +92 -0
  5. package/dist/src/config.js +3 -0
  6. package/dist/src/configured-declarations.js +232 -0
  7. package/dist/src/decorator.js +3 -10
  8. package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
  9. package/dist/src/findings/emitter.js +1 -0
  10. package/dist/src/findings/emitters.js +19 -0
  11. package/dist/src/findings/interfaces.js +2 -0
  12. package/dist/src/findings/intra-function.js +2 -0
  13. package/dist/src/findings/non-code-artifacts.js +2 -0
  14. package/dist/src/findings/reads-and-writes.js +2 -0
  15. package/dist/src/findings/self-check.js +2 -0
  16. package/dist/src/findings/unused-declarations.js +2 -0
  17. package/dist/src/findings/visibility-narrowing.js +2 -0
  18. package/dist/src/framework-lifecycle.js +192 -0
  19. package/dist/src/injection-container.js +101 -0
  20. package/dist/src/print.js +18 -0
  21. package/dist/src/resolve.js +98 -0
  22. package/dist/src/run.js +60 -176
  23. package/dist/src/schema.js +18 -1
  24. package/dist/src/serialization-contract.js +315 -0
  25. package/dist/src/value-reach.js +120 -0
  26. package/dist/src/verbs/print-config.js +9 -0
  27. package/dist/src/verbs/print-projects.js +34 -0
  28. package/dist/src/verbs/print-retained.js +23 -0
  29. package/dist/src/verbs/print-roots.js +36 -0
  30. package/dist/src/verbs/verb.js +5 -0
  31. package/dist/src/verbs/version.js +7 -0
  32. package/dist/src/version.js +1 -1
  33. package/jsr.json +1 -1
  34. package/package.json +2 -2
  35. package/src/analysis.ts +8 -1
  36. package/src/calls.ts +224 -0
  37. package/src/class-members.ts +136 -0
  38. package/src/config.ts +31 -0
  39. package/src/configured-declarations.ts +308 -0
  40. package/src/decorator.ts +4 -16
  41. package/src/exempt.ts +5 -1
  42. package/src/findings/dependencies-and-module-machinery.ts +4 -0
  43. package/src/findings/emitter.ts +19 -0
  44. package/src/findings/emitters.ts +21 -0
  45. package/src/findings/interfaces.ts +4 -0
  46. package/src/findings/intra-function.ts +4 -0
  47. package/src/findings/non-code-artifacts.ts +4 -0
  48. package/src/findings/reads-and-writes.ts +4 -0
  49. package/src/findings/self-check.ts +4 -0
  50. package/src/findings/unused-declarations.ts +4 -0
  51. package/src/findings/visibility-narrowing.ts +4 -0
  52. package/src/framework-lifecycle.ts +280 -0
  53. package/src/injection-container.ts +141 -0
  54. package/src/print.ts +20 -1
  55. package/src/resolve.ts +155 -0
  56. package/src/run.ts +64 -200
  57. package/src/schema.ts +19 -1
  58. package/src/serialization-contract.ts +492 -0
  59. package/src/value-reach.ts +152 -0
  60. package/src/verbs/print-config.ts +10 -0
  61. package/src/verbs/print-projects.ts +35 -0
  62. package/src/verbs/print-retained.ts +24 -0
  63. package/src/verbs/print-roots.ts +38 -0
  64. package/src/verbs/verb.ts +38 -0
  65. package/src/verbs/version.ts +8 -0
  66. package/src/version.ts +1 -1
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The classes of the target a value of one type carries, as a serializer walking the
3
+ * value reaches them: through union and intersection constituents, array and tuple
4
+ * elements, and the data members of the target's classes and of object types the
5
+ * target writes. The walk stops at `any`, `unknown`, a type parameter, a function and
6
+ * an interface or class of another program, so an instance held only there is missed.
7
+ */
8
+ import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
9
+ import { classNames, typesAt } from "./class-members.js";
10
+ /** The object types whose members the walk reads whatever declares them. */
11
+ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
12
+ /** The symbols of an anonymous object type the target writes as a type. */
13
+ const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
14
+ /** The global array types, whose element type is what an array value carries. */
15
+ function arrayTargets(project) {
16
+ const checker = project.checker;
17
+ return new Set(["Array", "ReadonlyArray"].flatMap((name) => {
18
+ const symbol = checker.resolveName(name, SymbolFlags.Type, undefined, false);
19
+ return symbol === undefined ? [] : [checker.getDeclaredTypeOfSymbol(symbol).id];
20
+ }));
21
+ }
22
+ /**
23
+ * The walk over one project's types, against the classes of `held`, the same
24
+ * project's inventory.
25
+ */
26
+ export function valueReach(project, held) {
27
+ const checker = project.checker;
28
+ const names = [...classNames(project, held)];
29
+ const declared = typesAt(project, names.map(([, node]) => node));
30
+ const classOfTarget = new Map();
31
+ names.forEach(([id], index) => {
32
+ const type = declared[index];
33
+ if (type !== undefined) {
34
+ classOfTarget.set(type.id, id);
35
+ }
36
+ });
37
+ const arrays = arrayTargets(project);
38
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
39
+ const children = new Map();
40
+ /** The generic target of a type reference, and the type itself otherwise. */
41
+ const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 && type.isTypeReference()
42
+ ? type.getTarget()
43
+ : type;
44
+ const classOf = (type) => {
45
+ if (!type.isObjectType()) {
46
+ return undefined;
47
+ }
48
+ return classOfTarget.get(type.id) ?? classOfTarget.get(targetOf(type).id);
49
+ };
50
+ /** Whether the target writes the object type, so its members are what the value holds. */
51
+ const isWritten = (type) => {
52
+ if ((type.objectFlags & WRITTEN) !== 0) {
53
+ return true;
54
+ }
55
+ const symbol = type.getSymbol();
56
+ if (symbol === undefined) {
57
+ return false;
58
+ }
59
+ if ((type.objectFlags & ObjectFlags.Anonymous) !== 0) {
60
+ return (symbol.flags & LITERAL_SYMBOLS) !== 0;
61
+ }
62
+ return ((symbol.flags & SymbolFlags.Interface) !== 0 &&
63
+ symbol.declarations.every((handle) => own.has(handle.path)));
64
+ };
65
+ /** The types a value of one object type holds in its data members. */
66
+ const memberTypes = (type) => {
67
+ const properties = checker
68
+ .getPropertiesOfType(type)
69
+ .filter((property) => (property.flags & SymbolFlags.Property) !== 0);
70
+ const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
71
+ return [
72
+ ...types.filter((held) => held !== undefined),
73
+ ...checker.getIndexInfosOfType(type).map((info) => info.valueType),
74
+ ];
75
+ };
76
+ /** The types a value of one type holds, read once per type. */
77
+ const childrenOf = (type) => {
78
+ const known = children.get(type.id);
79
+ if (known !== undefined) {
80
+ return known;
81
+ }
82
+ let found = [];
83
+ if (type.isUnionType() || type.isIntersectionType()) {
84
+ found = type.getTypes();
85
+ }
86
+ else if (type.isObjectType()) {
87
+ const target = targetOf(type);
88
+ const isList = type.isTypeReference() &&
89
+ (arrays.has(target.id) ||
90
+ (target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
91
+ if (classOf(type) !== undefined || (!isList && isWritten(type))) {
92
+ found = memberTypes(type);
93
+ }
94
+ else if (isList && type.isTypeReference()) {
95
+ found = checker.getTypeArguments(type);
96
+ }
97
+ }
98
+ children.set(type.id, found);
99
+ return found;
100
+ };
101
+ return {
102
+ classesOf: (type) => {
103
+ const found = new Set();
104
+ const seen = new Set();
105
+ const pending = [type];
106
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
107
+ if (seen.has(next.id)) {
108
+ continue;
109
+ }
110
+ seen.add(next.id);
111
+ const owner = classOf(next);
112
+ if (owner !== undefined) {
113
+ found.add(owner);
114
+ }
115
+ pending.push(...childrenOf(next));
116
+ }
117
+ return found;
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,9 @@
1
+ import { printConfig } from "../print.js";
2
+ import { resolve } from "../resolve.js";
3
+ import { EXIT_CLEAN } from "./verb.js";
4
+ /** Writes the resolved configuration with the source of every setting. */
5
+ export const printConfigVerb = ({ out, inputs }) => {
6
+ const { config, provenance } = resolve(inputs());
7
+ out.write(printConfig(config, provenance));
8
+ return EXIT_CLEAN;
9
+ };
@@ -0,0 +1,34 @@
1
+ import { diagnosticErrors, discoverProjects, renderDiagnostic } from "../discover.js";
2
+ import { resolveMatrix } from "../resolve.js";
3
+ import { diagnosticsOf, runSession } from "../session.js";
4
+ import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
5
+ /**
6
+ * Writes the name of every project one run analyzes, one per line, having read each
7
+ * project's diagnostics first: the identifier the build matrix declares for it, or,
8
+ * where discovery derived the project, its configuration file's path below the
9
+ * target root.
10
+ */
11
+ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient }) => {
12
+ const matrix = resolveMatrix(inputs());
13
+ const scoped = scope();
14
+ const engine = openClient(false);
15
+ const discovered = discoverProjects(engine, host, scoped, matrix);
16
+ const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
17
+ const failures = [];
18
+ const { projects } = runSession(engine, discovered.configFiles, (project) => {
19
+ const errors = diagnosticErrors(diagnosticsOf(project));
20
+ failures.push(...errors.map(renderDiagnostic));
21
+ return ids.get(project.configFile) ?? project.configFile;
22
+ });
23
+ if (failures.length > 0) {
24
+ for (const line of failures) {
25
+ err.write(`${line}\n`);
26
+ }
27
+ err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
28
+ return EXIT_FAILURE;
29
+ }
30
+ for (const id of projects) {
31
+ out.write(`${id}\n`);
32
+ }
33
+ return EXIT_CLEAN;
34
+ };
@@ -0,0 +1,23 @@
1
+ import { runSweep } from "../analysis.js";
2
+ import { retainedLines } from "../exempt.js";
3
+ import { resolve } from "../resolve.js";
4
+ import { EXIT_CLEAN } from "./verb.js";
5
+ /**
6
+ * Writes every declaration an exemption held back in the production sweep a report is
7
+ * built from, one per line, and nothing where no exemption held one back. One fact
8
+ * several configurations found is one record, so the line names no configuration. The
9
+ * line shape is this command's own rather than a Contract format, so that sort and cut
10
+ * read it.
11
+ */
12
+ export const printRetainedVerb = ({ out, host, inputs, scope, openClient }) => {
13
+ const { config } = resolve(inputs());
14
+ const scoped = scope();
15
+ const swept = runSweep(openClient(false), host, scoped, config, {
16
+ marked: [],
17
+ mode: { production: true },
18
+ });
19
+ for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
20
+ out.write(`${line}\n`);
21
+ }
22
+ return EXIT_CLEAN;
23
+ };
@@ -0,0 +1,36 @@
1
+ import { runRoots } from "../analysis.js";
2
+ import { resolve } from "../resolve.js";
3
+ import { EXIT_CLEAN, EXIT_FINDINGS } from "./verb.js";
4
+ /**
5
+ * One root as a line: the declaration's reference, why it is a root, and the string
6
+ * that named it, separated by tabs. A run of several configurations adds the ones
7
+ * whose project holds the root as a fourth field, and writes the third present and
8
+ * empty where no string named the root, so every line of one run holds the same
9
+ * fields.
10
+ */
11
+ function rootLine(root, several) {
12
+ const line = `${root.ref}\t${root.kind}`;
13
+ if (several) {
14
+ return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
15
+ }
16
+ return root.source === "" ? line : `${line}\t${root.source}`;
17
+ }
18
+ /**
19
+ * Writes the root set of the run, one root per line, and then names on the error
20
+ * stream, by its issue kind, every configured root or pattern that named nothing;
21
+ * one such string fails the run. The line shape is this command's own rather than a
22
+ * Contract format, so that sort and cut read it.
23
+ */
24
+ export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) => {
25
+ const { config, provenance } = resolve(inputs());
26
+ const scoped = scope();
27
+ const answer = runRoots(openClient(false), host, scoped, config, provenance);
28
+ const several = answer.configurations.length > 1;
29
+ for (const root of answer.roots) {
30
+ out.write(`${rootLine(root, several)}\n`);
31
+ }
32
+ for (const finding of answer.findings) {
33
+ err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
34
+ }
35
+ return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
36
+ };
@@ -0,0 +1,5 @@
1
+ /** The exit codes the Contract's exit-code table names. */
2
+ export const EXIT_CLEAN = 0;
3
+ export const EXIT_FINDINGS = 1;
4
+ export const EXIT_USAGE = 2;
5
+ export const EXIT_FAILURE = 3;
@@ -0,0 +1,7 @@
1
+ import { CONTRACT_VERSION } from "../version.js";
2
+ import { EXIT_CLEAN } from "./verb.js";
3
+ /** Writes the version the host reports and the Contract version this analyzer implements. */
4
+ export const versionVerb = ({ out, host }) => {
5
+ out.write(`deadset-ts ${host.analyzerVersion()}\ncontract ${CONTRACT_VERSION}\n`);
6
+ return EXIT_CLEAN;
7
+ };
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "3.0.0";
7
+ export const CONTRACT_VERSION = "3.1.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
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": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -37,7 +37,7 @@
37
37
  "fast-check": "4.10.2",
38
38
  "prettier": "3.9.9",
39
39
  "typescript": "npm:@typescript/typescript6@6.0.2",
40
- "typescript-eslint": "8.70.1",
40
+ "typescript-eslint": "8.71.0",
41
41
  "vitest": "5.0.2"
42
42
  },
43
43
  "publishConfig": {
package/src/analysis.ts CHANGED
@@ -22,7 +22,9 @@ import {
22
22
  } from "./exempt.ts";
23
23
  import type { TSExemptionClass } from "./exempt-classes.ts";
24
24
  import type { Finding } from "./finding.ts";
25
+ import { frameworkLifecycle } from "./framework-lifecycle.ts";
25
26
  import type { Host } from "./host.ts";
27
+ import { injectionContainer } from "./injection-container.ts";
26
28
  import { interfaceSatisfaction } from "./interface-satisfaction.ts";
27
29
  import { inventory, type Inventory } from "./inventory.ts";
28
30
  import { readManifest } from "./manifest.ts";
@@ -33,6 +35,7 @@ import { reflectiveLookup } from "./reflective-lookup.ts";
33
35
  import { references } from "./references.ts";
34
36
  import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
35
37
  import type { Scope } from "./scope.ts";
38
+ import { serializationContract } from "./serialization-contract.ts";
36
39
  import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
37
40
  import type { Exemption, SweepInput } from "./sweep.ts";
38
41
  import { readTemplates, templateField } from "./template-field.ts";
@@ -222,6 +225,9 @@ const DETECTORS: Detectors = new Map<TSExemptionClass, Detector>([
222
225
  ["template-field", templateField],
223
226
  ["reflective-lookup", reflectiveLookup],
224
227
  ["decorator", decorator],
228
+ ["injection-container", injectionContainer],
229
+ ["framework-lifecycle", frameworkLifecycle],
230
+ ["serialization-contract", serializationContract],
225
231
  ]);
226
232
 
227
233
  /**
@@ -247,6 +253,7 @@ export function runSweep(
247
253
  const templates = disabled.has("template-field")
248
254
  ? { delimiters: config.analysis.templateDelimiters, files: [] }
249
255
  : readTemplates(host, targetRoot, config.analysis);
256
+ const consumers = scope.consumers.map((consumer) => consumer.path);
250
257
  const read = readProjects(engine, host, scope, config, (project, projectRead) => {
251
258
  const resolved = references(project, projectRead.held, targetRoot, {
252
259
  testFiles: config.ts.testFiles,
@@ -259,7 +266,7 @@ export function runSweep(
259
266
  testFiles: resolved.testFilePaths,
260
267
  };
261
268
  const exempt = computeExemptions(
262
- { project, held: projectRead.held, targetRoot, templates },
269
+ { project, held: projectRead.held, targetRoot, templates, ts: config.ts, consumers },
263
270
  detectors,
264
271
  {
265
272
  disabled,
package/src/calls.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The calls and decorators of one project that the configured classes read: what each
3
+ * callee resolves to, and which of the target's classes a call passes or a decorator
4
+ * is attached to. A class is passed as an argument, or as an element of an array
5
+ * literal or a property value of an object literal written as one, at any depth, and a
6
+ * decorator written as a call is a call.
7
+ */
8
+
9
+ import {
10
+ isArrayLiteralExpression,
11
+ isCallExpression,
12
+ isClassDeclaration,
13
+ isDecorator,
14
+ isIdentifier,
15
+ isObjectLiteralExpression,
16
+ isParenthesizedExpression,
17
+ isPropertyAccessExpression,
18
+ isPropertyAssignment,
19
+ isShorthandPropertyAssignment,
20
+ isSpreadElement,
21
+ type CallExpression,
22
+ type Decorator,
23
+ type Expression,
24
+ type Node,
25
+ } from "@typescript/native/unstable/ast";
26
+ import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
27
+ import type { AliasChains } from "./alias-chain.ts";
28
+ import { nodeKey, type Inventory } from "./inventory.ts";
29
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
30
+ import type { ProjectView } from "./session.ts";
31
+
32
+ /** One value written where a call passes it, and the node its declaration is resolved from. */
33
+ export interface PassedValue {
34
+ /** Where the value is written, which is where a record of it sits. */
35
+ readonly site: Node;
36
+ /** The name the value is resolved through, or the shorthand assignment that holds it. */
37
+ readonly name: Node;
38
+ readonly shorthand: boolean;
39
+ }
40
+
41
+ /** One class of the target a call passes or a decorator is attached to, and where. */
42
+ export interface ClassUse {
43
+ /** The inventory's identifier of the class. */
44
+ readonly id: string;
45
+ readonly site: Node;
46
+ }
47
+
48
+ /** The name node a callee or a decorator expression resolves through, where it has one. */
49
+ export function calleeName(expression: Expression): Node | undefined {
50
+ if (isParenthesizedExpression(expression)) {
51
+ return calleeName(expression.expression);
52
+ }
53
+ if (isPropertyAccessExpression(expression)) {
54
+ return expression.name;
55
+ }
56
+ return isIdentifier(expression) ? expression : undefined;
57
+ }
58
+
59
+ /** The callee one decorator names: its expression, or the callee of the call it writes. */
60
+ export function decoratorCallee(decorator: Decorator): Node | undefined {
61
+ const expression = decorator.expression;
62
+ return calleeName(isCallExpression(expression) ? expression.expression : expression);
63
+ }
64
+
65
+ /** How one decorator is spelled in a record: its expression, without a call's arguments. */
66
+ export function spelledDecorator(decorator: Decorator): string {
67
+ const expression = decorator.expression;
68
+ const named = isCallExpression(expression) ? expression.expression : expression;
69
+ return `@${named.getText().replace(/\s+/gu, "")}`;
70
+ }
71
+
72
+ /** The decorators written on one declaration. */
73
+ export function decoratorsOf(node: Node): readonly Decorator[] {
74
+ return ((node as { readonly modifiers?: readonly Node[] }).modifiers ?? []).filter(isDecorator);
75
+ }
76
+
77
+ /**
78
+ * What each of a batch of nodes resolves to, every alias followed to the declaration
79
+ * it stands for. A node the checker resolves to nothing, and an alias that resolves
80
+ * to nothing, are absent from the answer. Each alias is followed once.
81
+ */
82
+ export function resolvedTargets<Brand>(
83
+ project: ProjectView<Brand>,
84
+ nodes: readonly Node[],
85
+ ): ReadonlyMap<Node, TSSymbol> {
86
+ const followed = new Map<number, TSSymbol | undefined>();
87
+ const targets = new Map<Node, TSSymbol>();
88
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
89
+ const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
90
+ project.symbolsAt(run.map((node) => project.handle(node))).forEach((symbol, index) => {
91
+ const node = run[index];
92
+ if (symbol === undefined || node === undefined) {
93
+ return;
94
+ }
95
+ let target: TSSymbol | undefined = symbol;
96
+ if ((symbol.flags & SymbolFlags.Alias) !== 0) {
97
+ if (!followed.has(symbol.id)) {
98
+ const aliased = project.checker.getAliasedSymbol(symbol);
99
+ followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
100
+ }
101
+ target = followed.get(symbol.id);
102
+ }
103
+ if (target !== undefined) {
104
+ targets.set(node, target);
105
+ }
106
+ });
107
+ }
108
+ return targets;
109
+ }
110
+
111
+ /** The values one expression passes: itself, or what an array or object literal holds. */
112
+ function passedIn(expression: Expression, found: PassedValue[]): void {
113
+ if (isParenthesizedExpression(expression)) {
114
+ passedIn(expression.expression, found);
115
+ return;
116
+ }
117
+ if (isArrayLiteralExpression(expression)) {
118
+ for (const element of expression.elements) {
119
+ passedIn(isSpreadElement(element) ? element.expression : element, found);
120
+ }
121
+ return;
122
+ }
123
+ if (isObjectLiteralExpression(expression)) {
124
+ for (const property of expression.properties) {
125
+ if (isPropertyAssignment(property)) {
126
+ passedIn(property.initializer, found);
127
+ } else if (isShorthandPropertyAssignment(property)) {
128
+ found.push({ site: property.name, name: property, shorthand: true });
129
+ }
130
+ }
131
+ return;
132
+ }
133
+ const name = calleeName(expression);
134
+ if (name !== undefined) {
135
+ found.push({ site: expression, name, shorthand: false });
136
+ }
137
+ }
138
+
139
+ /** The values one argument passes, a spread argument's included. */
140
+ export function valuesPassed(argument: Expression): readonly PassedValue[] {
141
+ const found: PassedValue[] = [];
142
+ passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
143
+ return found;
144
+ }
145
+
146
+ /**
147
+ * The classes of the target each call passes, by the call. A value that names no
148
+ * class of the target, a class of another program included, passes nothing here.
149
+ */
150
+ export function classesPassed<Brand>(
151
+ project: ProjectView<Brand>,
152
+ held: Inventory,
153
+ chains: AliasChains,
154
+ calls: readonly CallExpression[],
155
+ ): ReadonlyMap<CallExpression, readonly ClassUse[]> {
156
+ const passed = new Map(
157
+ calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]),
158
+ );
159
+ const all = [...passed.values()].flat();
160
+ const targets = resolvedTargets(
161
+ project,
162
+ all.filter((value) => !value.shorthand).map((value) => value.name),
163
+ );
164
+ const classes = new Set(
165
+ held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id),
166
+ );
167
+ const classesOf = (value: PassedValue): readonly string[] => {
168
+ const symbol = value.shorthand
169
+ ? project.shorthandValueAt(project.handle(value.name))
170
+ : targets.get(value.name);
171
+ return symbol === undefined
172
+ ? []
173
+ : chains.declarationsOf(symbol).filter((id) => classes.has(id));
174
+ };
175
+ return new Map(
176
+ [...passed].map(([call, values]) => [
177
+ call,
178
+ values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
179
+ ]),
180
+ );
181
+ }
182
+
183
+ /** One class declaration of the target and the decorators written on it. */
184
+ export interface DecoratedClass {
185
+ readonly id: string;
186
+ readonly decorators: readonly Decorator[];
187
+ }
188
+
189
+ /** Every class declaration of the target that carries a decorator. */
190
+ export function decoratedClasses<Brand>(
191
+ project: ProjectView<Brand>,
192
+ held: Inventory,
193
+ ): readonly DecoratedClass[] {
194
+ const found: DecoratedClass[] = [];
195
+ for (const file of project.ownSourceFiles()) {
196
+ const visit = (node: Node): void => {
197
+ if (isClassDeclaration(node)) {
198
+ const id = held.declarations.get(nodeKey(file, node));
199
+ const decorators = decoratorsOf(node);
200
+ if (id !== undefined && decorators.length > 0) {
201
+ found.push({ id, decorators });
202
+ }
203
+ }
204
+ node.forEachChild(visit);
205
+ };
206
+ file.forEachChild(visit);
207
+ }
208
+ return found;
209
+ }
210
+
211
+ /** Every call expression of the project's own files, decorators written as calls included. */
212
+ export function callsOf<Brand>(project: ProjectView<Brand>): readonly CallExpression[] {
213
+ const found: CallExpression[] = [];
214
+ for (const file of project.ownSourceFiles()) {
215
+ const visit = (node: Node): void => {
216
+ if (isCallExpression(node)) {
217
+ found.push(node);
218
+ }
219
+ node.forEachChild(visit);
220
+ };
221
+ file.forEachChild(visit);
222
+ }
223
+ return found;
224
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The members a value of one of the target's classes carries, as the checker answers
3
+ * for the class's type, so a member a base class declares is a member of every class
4
+ * that extends it and a member a subclass overrides is the subclass's own.
5
+ */
6
+
7
+ import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
8
+ import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
9
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
10
+ import { DEFAULT_BATCH_CAP } from "./references.ts";
11
+ import type { ProjectView } from "./session.ts";
12
+
13
+ /** One member of a class's value that the target declares. */
14
+ export interface ClassMember {
15
+ /** The inventory's identifier of the declaration. */
16
+ readonly id: string;
17
+ /** The declaring node, whose syntax says whether it is a property, an accessor or a method. */
18
+ readonly node: Node;
19
+ /** The member's symbol flags, as the checker reads it on this class. */
20
+ readonly flags: SymbolFlags;
21
+ }
22
+
23
+ /** Which side of a class a reading asks for: its instances, or the class itself. */
24
+ export type ClassSide = "instance" | "static";
25
+
26
+ /** The name node of each class declaration of the target, by the class's identifier. */
27
+ export function classNames<Brand>(
28
+ project: ProjectView<Brand>,
29
+ held: Inventory,
30
+ ): ReadonlyMap<string, Node> {
31
+ const names = new Map<string, Node>();
32
+ for (const file of project.ownSourceFiles()) {
33
+ const visit = (node: Node): void => {
34
+ if ((isClassDeclaration(node) || isClassExpression(node)) && node.name !== undefined) {
35
+ const id = held.declarations.get(nodeKey(file, node));
36
+ if (id !== undefined) {
37
+ names.set(id, node.name);
38
+ }
39
+ }
40
+ node.forEachChild(visit);
41
+ };
42
+ file.forEachChild(visit);
43
+ }
44
+ return names;
45
+ }
46
+
47
+ /** The types of a batch of nodes, asked in capped batches. */
48
+ export function typesAt<Brand>(
49
+ project: ProjectView<Brand>,
50
+ nodes: readonly Node[],
51
+ ): (Type | undefined)[] {
52
+ const types: (Type | undefined)[] = [];
53
+ for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
54
+ types.push(...project.checker.getTypeAtLocation(nodes.slice(from, from + DEFAULT_BATCH_CAP)));
55
+ }
56
+ return types;
57
+ }
58
+
59
+ /**
60
+ * The type one side of each class is: the instance type at the class's name, or the
61
+ * type of the class's own symbol, which is its constructor's.
62
+ */
63
+ function sideTypes<Brand>(
64
+ project: ProjectView<Brand>,
65
+ nodes: readonly Node[],
66
+ side: ClassSide,
67
+ ): (Type | undefined)[] {
68
+ if (side === "instance") {
69
+ return typesAt(project, nodes);
70
+ }
71
+ const symbols = project.symbolsAt(nodes.map((node) => project.handle(node)));
72
+ const present = symbols.filter((symbol): symbol is TSSymbol => symbol !== undefined);
73
+ const types = present.length === 0 ? [] : project.checker.getTypeOfSymbol(present);
74
+ let next = 0;
75
+ return symbols.map((symbol) => (symbol === undefined ? undefined : types[next++]));
76
+ }
77
+
78
+ /**
79
+ * The members one side of each class carries that the target declares, by the class.
80
+ * A member declared in a file outside the project's own, and the `prototype` a class
81
+ * carries on its static side, are not declarations of the target and are left out.
82
+ */
83
+ export function classMembers<Brand>(
84
+ project: ProjectView<Brand>,
85
+ held: Inventory,
86
+ classIds: ReadonlySet<string>,
87
+ side: ClassSide,
88
+ ): ReadonlyMap<string, readonly ClassMember[]> {
89
+ const named = [...classNames(project, held)].filter(([id]) => classIds.has(id));
90
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
91
+ const types = sideTypes(
92
+ project,
93
+ named.map(([, node]) => node),
94
+ side,
95
+ );
96
+ const members = new Map<string, readonly ClassMember[]>();
97
+ named.forEach(([id], index) => {
98
+ const type = types[index];
99
+ if (type === undefined) {
100
+ return;
101
+ }
102
+ const found: ClassMember[] = [];
103
+ for (const property of project.checker.getPropertiesOfType(type)) {
104
+ for (const handle of property.declarations) {
105
+ if (!own.has(handle.path)) {
106
+ continue;
107
+ }
108
+ const node = project.declarationAt(handle)?.node;
109
+ const declared =
110
+ node === undefined
111
+ ? undefined
112
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
113
+ if (node !== undefined && declared !== undefined) {
114
+ found.push({ id: declared, node, flags: property.flags });
115
+ }
116
+ }
117
+ }
118
+ members.set(id, found);
119
+ });
120
+ return members;
121
+ }
122
+
123
+ /**
124
+ * The last component of one member's stable reference, `:static` included, which is
125
+ * how a configuration names a member; undefined for a declaration that is no member.
126
+ */
127
+ export function memberComponent(
128
+ symbol: InventorySymbol,
129
+ byId: ReadonlyMap<string, InventorySymbol>,
130
+ ): string | undefined {
131
+ const parent = byId.get(symbol.parent);
132
+ if (parent === undefined || !symbol.ref.startsWith(`${parent.ref}.`)) {
133
+ return undefined;
134
+ }
135
+ return symbol.ref.slice(parent.ref.length + 1);
136
+ }