@cplieger/deadset-ts 3.2.0 → 3.3.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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/src/analysis.js +14 -2
  3. package/dist/src/decorator.js +64 -0
  4. package/dist/src/exempt-classes.js +57 -10
  5. package/dist/src/exempt.js +23 -3
  6. package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
  7. package/dist/src/findings/emitter.js +1 -0
  8. package/dist/src/findings/emitters.js +19 -0
  9. package/dist/src/findings/interfaces.js +2 -0
  10. package/dist/src/findings/intra-function.js +2 -0
  11. package/dist/src/findings/non-code-artifacts.js +2 -0
  12. package/dist/src/findings/reads-and-writes.js +2 -0
  13. package/dist/src/findings/self-check.js +2 -0
  14. package/dist/src/findings/unused-declarations.js +2 -0
  15. package/dist/src/findings/visibility-narrowing.js +2 -0
  16. package/dist/src/interface-satisfaction.js +310 -0
  17. package/dist/src/member-names.js +76 -0
  18. package/dist/src/references.js +22 -3
  19. package/dist/src/reflective-lookup.js +96 -0
  20. package/dist/src/run.js +60 -176
  21. package/dist/src/template-field.js +173 -0
  22. package/dist/src/verbs/print-config.js +9 -0
  23. package/dist/src/verbs/print-projects.js +34 -0
  24. package/dist/src/verbs/print-retained.js +23 -0
  25. package/dist/src/verbs/print-roots.js +36 -0
  26. package/dist/src/verbs/verb.js +5 -0
  27. package/dist/src/verbs/version.js +7 -0
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +24 -6
  31. package/src/decorator.ts +74 -0
  32. package/src/exempt-classes.ts +75 -12
  33. package/src/exempt.ts +29 -3
  34. package/src/findings/dependencies-and-module-machinery.ts +4 -0
  35. package/src/findings/emitter.ts +19 -0
  36. package/src/findings/emitters.ts +21 -0
  37. package/src/findings/interfaces.ts +4 -0
  38. package/src/findings/intra-function.ts +4 -0
  39. package/src/findings/non-code-artifacts.ts +4 -0
  40. package/src/findings/reads-and-writes.ts +4 -0
  41. package/src/findings/self-check.ts +4 -0
  42. package/src/findings/unused-declarations.ts +4 -0
  43. package/src/findings/visibility-narrowing.ts +4 -0
  44. package/src/interface-satisfaction.ts +417 -0
  45. package/src/member-names.ts +89 -0
  46. package/src/references.ts +41 -6
  47. package/src/reflective-lookup.ts +127 -0
  48. package/src/run.ts +64 -200
  49. package/src/template-field.ts +214 -0
  50. package/src/verbs/print-config.ts +10 -0
  51. package/src/verbs/print-projects.ts +35 -0
  52. package/src/verbs/print-retained.ts +24 -0
  53. package/src/verbs/print-roots.ts +38 -0
  54. package/src/verbs/verb.ts +38 -0
  55. package/src/verbs/version.ts +8 -0
package/README.md CHANGED
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
8
8
 
9
9
  deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
10
10
 
11
- **Status: pre-release.** This version implements contract 3.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
11
+ **Status: pre-release.** This version implements contract 3.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Four exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
12
12
 
13
13
  ## Install
14
14
 
@@ -28,7 +28,7 @@ The npm package carries the command and the library as JavaScript that the compi
28
28
  npx deadset-ts version
29
29
  ```
30
30
 
31
- `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class. `analyze`, `explain`, `describe`, any other invocation and no arguments print the usage line and exit 2.
31
+ `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze`, `explain`, `describe`, any other invocation and no arguments print the usage line and exit 2.
32
32
 
33
33
  | Verb | Job |
34
34
  | ---------------- | ------------------------------------------------------------------------------- |
@@ -7,16 +7,20 @@
7
7
  * carries a project's identity in its type so a handle cannot cross to another
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
+ import { decorator } from "./decorator.js";
10
11
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
11
12
  import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
13
+ import { interfaceSatisfaction } from "./interface-satisfaction.js";
12
14
  import { inventory } from "./inventory.js";
13
15
  import { readManifest } from "./manifest.js";
14
16
  import { matrixOf, sweepMatrix } from "./matrix.js";
15
17
  import { relativePath, resolvePath } from "./paths.js";
16
18
  import { byPosition } from "./position.js";
19
+ import { reflectiveLookup } from "./reflective-lookup.js";
17
20
  import { references } from "./references.js";
18
21
  import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
19
22
  import { diagnosticsOf, runSession } from "./session.js";
23
+ import { readTemplates, templateField } from "./template-field.js";
20
24
  /** The setting whose source decides where a finding about a configured root sits. */
21
25
  const ROOTS_SETTING = "roots.patterns";
22
26
  /**
@@ -119,7 +123,12 @@ export function runRoots(engine, host, scope, config, provenance) {
119
123
  };
120
124
  }
121
125
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
122
- const DETECTORS = new Map();
126
+ const DETECTORS = new Map([
127
+ ["interface-satisfaction", interfaceSatisfaction],
128
+ ["template-field", templateField],
129
+ ["reflective-lookup", reflectiveLookup],
130
+ ["decorator", decorator],
131
+ ]);
123
132
  /**
124
133
  * Sweeps the run the scope and the configuration describe. Each project's references
125
134
  * and exemptions are read against its own inventory while its view is open; the
@@ -133,6 +142,9 @@ const DETECTORS = new Map();
133
142
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
134
143
  const targetRoot = scope.target.path;
135
144
  const disabled = disabledClasses(config);
145
+ const templates = disabled.has("template-field")
146
+ ? { delimiters: config.analysis.templateDelimiters, files: [] }
147
+ : readTemplates(host, targetRoot, config.analysis);
136
148
  const read = readProjects(engine, host, scope, config, (project, projectRead) => {
137
149
  const resolved = references(project, projectRead.held, targetRoot, {
138
150
  testFiles: config.ts.testFiles,
@@ -144,7 +156,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
144
156
  roots: projectRead.rooted.liveUnderReachability,
145
157
  testFiles: resolved.testFilePaths,
146
158
  };
147
- const exempt = computeExemptions({ project, held: projectRead.held, targetRoot }, detectors, {
159
+ const exempt = computeExemptions({ project, held: projectRead.held, targetRoot, templates }, detectors, {
148
160
  disabled,
149
161
  mode: input.mode,
150
162
  testFiles: new Set(resolved.testFilePaths),
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The decorator class: a member a decorator expression is attached to, and every
3
+ * member of a class a decorator expression is attached to, is held back, because the
4
+ * decorator receives the member or the class when the class is defined and may reach
5
+ * the member by its name.
6
+ *
7
+ * The rule is read from the tree alone: a decorator is attached where it is written.
8
+ * The use the decorator itself makes of what it decorates is the reference pass's,
9
+ * which records it as a `decorator` reference; this class records only that the
10
+ * declaration is held back, and the member set of a decorated class is every member
11
+ * the inventory holds for the class, static members included.
12
+ */
13
+ import { isCallExpression, isDecorator, } from "@typescript/native/unstable/ast";
14
+ import { nodeKey } from "./inventory.js";
15
+ import { renderPosition } from "./position.js";
16
+ /** The kinds of declaration a decorator is attached to and holds back. */
17
+ const DECORATED = new Set(["class", "method", "class-member"]);
18
+ /** How one decorator is spelled in a record: its expression, without a call's arguments. */
19
+ function spelled(decorator) {
20
+ const expression = decorator.expression;
21
+ const named = isCallExpression(expression) ? expression.expression : expression;
22
+ return `@${named.getText().replace(/\s+/gu, "")}`;
23
+ }
24
+ /**
25
+ * The decorator detector: each decorated member, and each member of a decorated
26
+ * class, recorded at the decorator.
27
+ */
28
+ export function decorator(input) {
29
+ const { project, held, targetRoot } = input;
30
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
31
+ const membersOf = new Map();
32
+ for (const symbol of held.symbols) {
33
+ if (symbol.kind === "method" || symbol.kind === "class-member") {
34
+ const members = membersOf.get(symbol.parent);
35
+ if (members === undefined) {
36
+ membersOf.set(symbol.parent, [symbol]);
37
+ }
38
+ else {
39
+ members.push(symbol);
40
+ }
41
+ }
42
+ }
43
+ const found = [];
44
+ for (const file of project.ownSourceFiles()) {
45
+ const visit = (node) => {
46
+ const id = held.declarations.get(nodeKey(file, node));
47
+ const symbol = id === undefined ? undefined : byId.get(id);
48
+ if (symbol !== undefined && DECORATED.has(symbol.kind)) {
49
+ const modifiers = node.modifiers ?? [];
50
+ for (const attached of modifiers.filter(isDecorator)) {
51
+ const detail = `decorated by ${spelled(attached)}`;
52
+ const site = renderPosition(file, targetRoot, attached.getStart());
53
+ const reached = symbol.kind === "class" ? (membersOf.get(symbol.id) ?? []) : [symbol];
54
+ for (const member of reached) {
55
+ found.push({ id: member.id, detail, site });
56
+ }
57
+ }
58
+ }
59
+ node.forEachChild(visit);
60
+ };
61
+ file.forEachChild(visit);
62
+ }
63
+ return found;
64
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The exemption classes of the Contract's vocabulary, in its order: each class's name,
3
- * the languages it runs on and the confidence it is recorded at.
3
+ * the languages it runs on, the confidence it is recorded at and, for a class that runs
4
+ * on TypeScript, which private members it may retain.
4
5
  *
5
6
  * The rows are written out rather than read from the Contract at run time, for the
6
7
  * reason the fixed severity codes are, and a test holds them equal to the Contract
@@ -10,26 +11,72 @@
10
11
  */
11
12
  /** Every class of the vocabulary, in the order the Contract lists them. */
12
13
  export const EXEMPTION_CLASSES = [
13
- { class: "interface-satisfaction", languages: ["go", "ts"], confidence: "certain" },
14
+ {
15
+ class: "interface-satisfaction",
16
+ languages: ["go", "ts"],
17
+ confidence: "certain",
18
+ typescriptVisibility: { private: false, privateName: false },
19
+ },
14
20
  { class: "encoding-reflection", languages: ["go"], confidence: "certain" },
15
21
  { class: "format-verb-contract", languages: ["go"], confidence: "certain" },
16
22
  { class: "errors-duck-typing", languages: ["go"], confidence: "certain" },
17
- { class: "enum-group", languages: ["go", "ts"], confidence: "certain" },
23
+ {
24
+ class: "enum-group",
25
+ languages: ["go", "ts"],
26
+ confidence: "certain",
27
+ typescriptVisibility: { private: false, privateName: false },
28
+ },
18
29
  { class: "generated-file", languages: ["go"], confidence: "certain" },
19
30
  { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
20
- { class: "template-field", languages: ["go", "ts"], confidence: "possible" },
21
- { class: "reflective-lookup", languages: ["go", "ts"], confidence: "possible" },
22
- { class: "decorator", languages: ["ts"], confidence: "certain" },
23
- { class: "injection-container", languages: ["ts"], confidence: "certain" },
24
- { class: "framework-lifecycle", languages: ["ts"], confidence: "certain" },
25
- { class: "serialization-contract", languages: ["ts"], confidence: "certain" },
31
+ {
32
+ class: "template-field",
33
+ languages: ["go", "ts"],
34
+ confidence: "possible",
35
+ typescriptVisibility: { private: true, privateName: false },
36
+ },
37
+ {
38
+ class: "reflective-lookup",
39
+ languages: ["go", "ts"],
40
+ confidence: "possible",
41
+ typescriptVisibility: { private: true, privateName: false },
42
+ },
43
+ {
44
+ class: "decorator",
45
+ languages: ["ts"],
46
+ confidence: "certain",
47
+ typescriptVisibility: { private: true, privateName: false },
48
+ },
49
+ {
50
+ class: "injection-container",
51
+ languages: ["ts"],
52
+ confidence: "certain",
53
+ typescriptVisibility: { private: true, privateName: false },
54
+ },
55
+ {
56
+ class: "framework-lifecycle",
57
+ languages: ["ts"],
58
+ confidence: "certain",
59
+ typescriptVisibility: { private: true, privateName: false },
60
+ },
61
+ {
62
+ class: "serialization-contract",
63
+ languages: ["ts"],
64
+ confidence: "certain",
65
+ typescriptVisibility: { private: true, privateName: false },
66
+ },
26
67
  ];
27
68
  function runsOnTS(row) {
28
69
  const languages = row.languages;
29
70
  return languages.includes("ts");
30
71
  }
72
+ const TS_ROWS = EXEMPTION_CLASSES.filter(runsOnTS);
31
73
  /** The classes that run on TypeScript, in the vocabulary's order. */
32
- export const TS_EXEMPTION_CLASSES = EXEMPTION_CLASSES.filter(runsOnTS).map((row) => row.class);
74
+ export const TS_EXEMPTION_CLASSES = TS_ROWS.map((row) => row.class);
75
+ const VISIBILITY = new Map(TS_ROWS.map((row) => [row.class, row.typescriptVisibility]));
76
+ /** Which private members one class that runs on TypeScript may retain. */
77
+ export function typescriptVisibilityOf(exemptionClass) {
78
+ return VISIBILITY.get(exemptionClass) ?? { private: false, privateName: false };
79
+ }
33
80
  /** Whether one name is a class of the vocabulary, whichever language it runs on. */
34
81
  export function isExemptionClass(name) {
35
82
  return EXEMPTION_CLASSES.some((row) => row.class === name);
@@ -7,7 +7,7 @@
7
7
  * hands it the table of the classes it has.
8
8
  */
9
9
  import { ConfigError } from "./config.js";
10
- import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, } from "./exempt-classes.js";
10
+ import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
11
11
  import { byPosition, positionKey } from "./position.js";
12
12
  import { sweep } from "./sweep.js";
13
13
  /**
@@ -26,19 +26,39 @@ export function disabledClasses(config) {
26
26
  }
27
27
  return disabled;
28
28
  }
29
+ /**
30
+ * Whether a class whose row states `allowed` may retain a member of one visibility. A
31
+ * declaration the inventory does not hold has no visibility to refuse it by.
32
+ */
33
+ function retainable(allowed, visibility) {
34
+ if (visibility === "private") {
35
+ return allowed.private;
36
+ }
37
+ if (visibility === "private-name") {
38
+ return allowed.privateName;
39
+ }
40
+ return true;
41
+ }
29
42
  /**
30
43
  * The records one project's detectors found, each class in the vocabulary's order and
31
- * none of a switched-off class. Under a production mode a record whose evidence is
32
- * written in a test file holds nothing, as a reference a test file makes is none there.
44
+ * none of a switched-off class. Evidence on a private member the class's row says it
45
+ * cannot retain is dropped first, whatever the detector found. Under a production mode
46
+ * a record whose evidence is written in a test file holds nothing, as a reference a
47
+ * test file makes is none there.
33
48
  */
34
49
  export function computeExemptions(input, detectors, holding) {
50
+ const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
35
51
  const found = [];
36
52
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
37
53
  const detect = detectors.get(exemptionClass);
38
54
  if (detect === undefined || holding.disabled.has(exemptionClass)) {
39
55
  continue;
40
56
  }
57
+ const allowed = typescriptVisibilityOf(exemptionClass);
41
58
  for (const evidence of detect(input)) {
59
+ if (!retainable(allowed, visibility.get(evidence.id))) {
60
+ continue;
61
+ }
42
62
  if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
43
63
  continue;
44
64
  }
@@ -0,0 +1,2 @@
1
+ /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
2
+ export const dependenciesAndModuleMachinery = () => [];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machinery.js";
2
+ import { interfaces } from "./interfaces.js";
3
+ import { intraFunction } from "./intra-function.js";
4
+ import { nonCodeArtifacts } from "./non-code-artifacts.js";
5
+ import { readsAndWrites } from "./reads-and-writes.js";
6
+ import { selfCheck } from "./self-check.js";
7
+ import { unusedDeclarations } from "./unused-declarations.js";
8
+ import { visibilityNarrowing } from "./visibility-narrowing.js";
9
+ /** Every kind family this analyzer reports, each by its emitter, in code order. */
10
+ export const EMITTERS = new Map([
11
+ ["unused-declarations", unusedDeclarations],
12
+ ["visibility-narrowing", visibilityNarrowing],
13
+ ["interfaces", interfaces],
14
+ ["reads-and-writes", readsAndWrites],
15
+ ["non-code-artifacts", nonCodeArtifacts],
16
+ ["dependencies-and-module-machinery", dependenciesAndModuleMachinery],
17
+ ["self-check", selfCheck],
18
+ ["intra-function", intraFunction],
19
+ ]);
@@ -0,0 +1,2 @@
1
+ /** The findings of the interfaces family, `DS1200` to `DS1299`. */
2
+ export const interfaces = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the intra-function family, `DS1800` to `DS1899`. */
2
+ export const intraFunction = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the non-code-artifacts family, `DS1500` to `DS1599`. */
2
+ export const nonCodeArtifacts = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the reads-and-writes family, `DS1300` to `DS1399`. */
2
+ export const readsAndWrites = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the self-check family, `DS1700` to `DS1799`. */
2
+ export const selfCheck = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
2
+ export const unusedDeclarations = () => [];
@@ -0,0 +1,2 @@
1
+ /** The findings of the visibility-narrowing family, `DS1100` to `DS1199`. */
2
+ export const visibilityNarrowing = () => [];
@@ -0,0 +1,310 @@
1
+ /**
2
+ * The interface-satisfaction class: a class member is held back when a value of the
3
+ * class reaches a position the checker types as an interface requiring a member of
4
+ * that name, so a caller reaches the member through the interface while no name in
5
+ * the program resolves to it.
6
+ *
7
+ * A value reaches such a position as an `implements` entry, the initializer of a
8
+ * declaration whose type is written, an assignment, an argument, a returned value, an
9
+ * array element, an object property, or the operand of `as` or `satisfies`, read
10
+ * through parentheses, conditionals and the operators whose result is an operand.
11
+ * `this` in an instance member is a value of its class. Types are asked in batches;
12
+ * the contextual lookup has none, so it is asked only of a value whose type is a class
13
+ * of the target, and each distinct pair, interface and class is asked once.
14
+ */
15
+ import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isConditionalExpression, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isIdentifier, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
16
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
17
+ import { aliasChains } from "./alias-chain.js";
18
+ import { nodeKey } from "./inventory.js";
19
+ import { renderPosition } from "./position.js";
20
+ import { DEFAULT_BATCH_CAP } from "./references.js";
21
+ /** The operators of an assignment whose right side is stored into the left side's type. */
22
+ const STORES = new Set([
23
+ SyntaxKind.EqualsToken,
24
+ SyntaxKind.QuestionQuestionEqualsToken,
25
+ SyntaxKind.BarBarEqualsToken,
26
+ SyntaxKind.AmpersandAmpersandEqualsToken,
27
+ ]);
28
+ /** Whether one node declares a static member, whose `this` is the class itself. */
29
+ function isStatic(node) {
30
+ const flags = node.modifierFlags ?? 0;
31
+ return (flags & ModifierFlags.Static) !== 0;
32
+ }
33
+ /** Whether one node is a method or an accessor of an object literal, whose `this` is the literal. */
34
+ function isObjectLiteralMethod(node) {
35
+ return ((isMethodDeclaration(node) ||
36
+ isGetAccessorDeclaration(node) ||
37
+ isSetAccessorDeclaration(node)) &&
38
+ isObjectLiteralExpression(node.parent));
39
+ }
40
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
41
+ function writesType(node) {
42
+ return node.type !== undefined;
43
+ }
44
+ /**
45
+ * The values one written expression hands on: the expression itself, or what it
46
+ * evaluates to through parentheses, a conditional and the operators whose result is
47
+ * one of their operands.
48
+ */
49
+ function valuesOf(expression) {
50
+ if (isParenthesizedExpression(expression)) {
51
+ return valuesOf(expression.expression);
52
+ }
53
+ if (isConditionalExpression(expression)) {
54
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
55
+ }
56
+ if (isBinaryExpression(expression)) {
57
+ const operator = expression.operatorToken.kind;
58
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
59
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
60
+ }
61
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
62
+ return valuesOf(expression.right);
63
+ }
64
+ }
65
+ return [expression];
66
+ }
67
+ /** The expressions one node hands to a position typed by its context. */
68
+ function handedOn(node) {
69
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
70
+ node.initializer !== undefined &&
71
+ writesType(node)) {
72
+ return [node.initializer];
73
+ }
74
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
75
+ return [node.right];
76
+ }
77
+ if (isCallExpression(node) || isNewExpression(node)) {
78
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
79
+ }
80
+ if (isReturnStatement(node)) {
81
+ return node.expression === undefined ? [] : [node.expression];
82
+ }
83
+ if (isArrowFunction(node)) {
84
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
85
+ }
86
+ if (isArrayLiteralExpression(node)) {
87
+ return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
88
+ }
89
+ if (isPropertyAssignment(node)) {
90
+ return [node.initializer];
91
+ }
92
+ if (isShorthandPropertyAssignment(node)) {
93
+ return isIdentifier(node.name) ? [node.name] : [];
94
+ }
95
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
96
+ return [node.expression];
97
+ }
98
+ return [];
99
+ }
100
+ /**
101
+ * The classes, the `implements` entries and the flowing values of one project's own
102
+ * files. `this` is the class an instance member belongs to, so a function that is not
103
+ * an arrow, a static member and a static block each stop it.
104
+ */
105
+ function walk(files, declarations) {
106
+ const classes = [];
107
+ const clauses = [];
108
+ const flows = [];
109
+ for (const file of files) {
110
+ const visit = (node, self) => {
111
+ let inner = self;
112
+ if (isClassDeclaration(node) || isClassExpression(node)) {
113
+ const id = declarations.get(nodeKey(file, node));
114
+ const name = node.name;
115
+ const owner = id === undefined || name === undefined
116
+ ? undefined
117
+ : { id, name, generic: node.typeParameters !== undefined };
118
+ if (owner !== undefined) {
119
+ classes.push(owner);
120
+ for (const clause of node.heritageClauses ?? []) {
121
+ if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
122
+ clauses.push(...clause.types.map((entry) => ({ owner, entry })));
123
+ }
124
+ }
125
+ }
126
+ inner = owner;
127
+ }
128
+ else if (isFunctionDeclaration(node) ||
129
+ isFunctionExpression(node) ||
130
+ isClassStaticBlockDeclaration(node) ||
131
+ isStatic(node) ||
132
+ isObjectLiteralMethod(node)) {
133
+ inner = undefined;
134
+ }
135
+ for (const written of handedOn(node)) {
136
+ for (const value of valuesOf(written)) {
137
+ flows.push({
138
+ value,
139
+ self: value.kind === SyntaxKind.ThisKeyword ? inner : undefined,
140
+ });
141
+ }
142
+ }
143
+ node.forEachChild((child) => {
144
+ visit(child, inner);
145
+ });
146
+ };
147
+ file.forEachChild((child) => {
148
+ visit(child, undefined);
149
+ });
150
+ }
151
+ return { classes, clauses, flows };
152
+ }
153
+ /**
154
+ * The interface-satisfaction detector: for each class type that reaches an interface
155
+ * type it is assignable to, the class's members whose names the interface's members
156
+ * carry, each recorded at the place the value flows.
157
+ */
158
+ export function interfaceSatisfaction(input) {
159
+ const { project, held, targetRoot } = input;
160
+ const checker = project.checker;
161
+ const files = project.ownSourceFiles();
162
+ const { classes, clauses, flows } = walk(files, held.declarations);
163
+ if (classes.length === 0) {
164
+ return [];
165
+ }
166
+ // One list, so the batches are capped runs of the whole project rather than of a
167
+ // file: a class's name, an `implements` entry and a value are each one question.
168
+ const asked = [
169
+ ...classes.map((owner) => owner.name),
170
+ ...clauses.map((clause) => clause.entry),
171
+ ...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
172
+ ];
173
+ const typeAt = new Map();
174
+ for (let from = 0; from < asked.length; from += DEFAULT_BATCH_CAP) {
175
+ const run = asked.slice(from, from + DEFAULT_BATCH_CAP);
176
+ checker.getTypeAtLocation(run).forEach((type, index) => {
177
+ const node = run[index];
178
+ if (type !== undefined && node !== undefined) {
179
+ typeAt.set(node, type);
180
+ }
181
+ });
182
+ }
183
+ const classOfType = new Map();
184
+ const instanceOf = new Map();
185
+ for (const owner of classes) {
186
+ const type = typeAt.get(owner.name);
187
+ if (type !== undefined) {
188
+ classOfType.set(type.id, owner);
189
+ instanceOf.set(owner.id, type);
190
+ }
191
+ }
192
+ const instantiates = classes.some((owner) => owner.generic);
193
+ const constituents = new Map();
194
+ const partsOf = (type) => {
195
+ if (!type.isUnionType() && !type.isIntersectionType()) {
196
+ return [type];
197
+ }
198
+ let parts = constituents.get(type.id);
199
+ if (parts === undefined) {
200
+ parts = type.getTypes();
201
+ constituents.set(type.id, parts);
202
+ }
203
+ return parts;
204
+ };
205
+ const targets = new Map();
206
+ /** The classes of the target one value's type is an instance of, with the instance type. */
207
+ const classesOf = (type) => partsOf(type).flatMap((part) => {
208
+ const direct = classOfType.get(part.id);
209
+ if (direct !== undefined) {
210
+ return [{ owner: direct, type: part }];
211
+ }
212
+ if (!instantiates || !part.isTypeReference()) {
213
+ return [];
214
+ }
215
+ if (!targets.has(part.id)) {
216
+ targets.set(part.id, classOfType.get(part.getTarget().id));
217
+ }
218
+ const generic = targets.get(part.id);
219
+ return generic === undefined ? [] : [{ owner: generic, type: part }];
220
+ });
221
+ const interfaceNames = new Map();
222
+ /** The interface types one position's type is or holds, each with the interface's name. */
223
+ const interfacesOf = (type) => partsOf(type).flatMap((part) => {
224
+ if (!interfaceNames.has(part.id)) {
225
+ const symbol = part.isObjectType() ? part.getSymbol() : undefined;
226
+ interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
227
+ ? symbol.name
228
+ : undefined);
229
+ }
230
+ const name = interfaceNames.get(part.id);
231
+ return name === undefined ? [] : [{ type: part, name }];
232
+ });
233
+ const pairs = new Map();
234
+ const reach = (owner, source, target, name, site) => {
235
+ const key = `${String(source.id)}:${String(target.id)}`;
236
+ const pair = pairs.get(key);
237
+ if (pair === undefined) {
238
+ pairs.set(key, { owner, source, target, name, sites: [site] });
239
+ }
240
+ else {
241
+ pair.sites.push(site);
242
+ }
243
+ };
244
+ for (const clause of clauses) {
245
+ const source = instanceOf.get(clause.owner.id);
246
+ const target = typeAt.get(clause.entry);
247
+ if (source === undefined || target === undefined) {
248
+ continue;
249
+ }
250
+ for (const reached of interfacesOf(target)) {
251
+ reach(clause.owner, source, reached.type, reached.name, clause.entry);
252
+ }
253
+ }
254
+ for (const flow of flows) {
255
+ const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
256
+ const valueType = typeAt.get(flow.value);
257
+ const sources = flow.self !== undefined && self !== undefined
258
+ ? [{ owner: flow.self, type: self }]
259
+ : valueType === undefined
260
+ ? []
261
+ : classesOf(valueType);
262
+ if (sources.length === 0) {
263
+ continue;
264
+ }
265
+ const position = checker.getContextualType(flow.value);
266
+ if (position === undefined) {
267
+ continue;
268
+ }
269
+ for (const reached of interfacesOf(position)) {
270
+ for (const source of sources) {
271
+ reach(source.owner, source.type, reached.type, reached.name, flow.value);
272
+ }
273
+ }
274
+ }
275
+ const chains = aliasChains(project, held);
276
+ const required = new Map();
277
+ const members = new Map();
278
+ const found = [];
279
+ for (const pair of pairs.values()) {
280
+ if (!checker.isTypeAssignableTo(pair.source, pair.target)) {
281
+ continue;
282
+ }
283
+ let names = required.get(pair.target.id);
284
+ if (names === undefined) {
285
+ names = new Set(checker.getPropertiesOfType(pair.target).map((property) => property.escapedName));
286
+ required.set(pair.target.id, names);
287
+ }
288
+ let answering = members.get(pair.owner.id);
289
+ if (answering === undefined) {
290
+ const instance = instanceOf.get(pair.owner.id) ?? pair.source;
291
+ answering = new Map(checker
292
+ .getPropertiesOfType(instance)
293
+ .map((property) => [property.escapedName, chains.declarationsOf(property)]));
294
+ members.set(pair.owner.id, answering);
295
+ }
296
+ const detail = `satisfies ${pair.name}`;
297
+ for (const name of names) {
298
+ for (const id of answering.get(name) ?? []) {
299
+ for (const site of pair.sites) {
300
+ found.push({
301
+ id,
302
+ detail,
303
+ site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
304
+ });
305
+ }
306
+ }
307
+ }
308
+ }
309
+ return found;
310
+ }