@cplieger/deadset-ts 3.13.0 → 4.0.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 (92) hide show
  1. package/README.md +67 -49
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +120 -24
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/configured-declarations.js +22 -9
  8. package/dist/src/dependencies.js +34 -17
  9. package/dist/src/deprecation.js +35 -24
  10. package/dist/src/destructuring.js +37 -29
  11. package/dist/src/discover.js +106 -29
  12. package/dist/src/entry-point-gaps.js +2 -2
  13. package/dist/src/entry-points.js +7 -0
  14. package/dist/src/enum-group.js +46 -40
  15. package/dist/src/exempt.js +42 -1
  16. package/dist/src/findings/emitters.js +15 -1
  17. package/dist/src/findings/interfaces.js +7 -1
  18. package/dist/src/framework-lifecycle.js +8 -6
  19. package/dist/src/implementations.js +45 -23
  20. package/dist/src/interface-satisfaction.js +30 -27
  21. package/dist/src/intra-function-parts.js +37 -20
  22. package/dist/src/inventory.js +85 -4
  23. package/dist/src/json-chunks.js +76 -0
  24. package/dist/src/partial.js +23 -0
  25. package/dist/src/query.js +207 -0
  26. package/dist/src/references.js +92 -20
  27. package/dist/src/reflective-lookup.js +18 -9
  28. package/dist/src/report.js +3 -1
  29. package/dist/src/reporters/annotations.js +9 -5
  30. package/dist/src/reporters/json.js +3 -2
  31. package/dist/src/reporters/sarif.js +2 -1
  32. package/dist/src/reporters/template.js +51 -10
  33. package/dist/src/reporters/text.js +8 -6
  34. package/dist/src/roots.js +27 -11
  35. package/dist/src/schema.js +2 -2
  36. package/dist/src/serialization-contract.js +12 -10
  37. package/dist/src/session.js +79 -39
  38. package/dist/src/suppress-file.js +2 -1
  39. package/dist/src/sweep.js +4 -1
  40. package/dist/src/value-reach.js +14 -19
  41. package/dist/src/verbs/analyze.js +20 -3
  42. package/dist/src/verbs/explain.js +22 -9
  43. package/dist/src/verbs/print-projects.js +41 -8
  44. package/dist/src/verbs/print-retained.js +6 -2
  45. package/dist/src/verbs/print-roots.js +5 -1
  46. package/jsr.json +1 -1
  47. package/package.json +3 -3
  48. package/src/alias-chain.ts +75 -11
  49. package/src/analysis.ts +224 -62
  50. package/src/calls.ts +10 -9
  51. package/src/class-members.ts +10 -11
  52. package/src/configured-declarations.ts +29 -13
  53. package/src/dependencies.ts +42 -20
  54. package/src/deprecation.ts +53 -27
  55. package/src/destructuring.ts +50 -39
  56. package/src/discover.ts +178 -34
  57. package/src/entry-point-gaps.ts +2 -2
  58. package/src/entry-points.ts +7 -0
  59. package/src/enum-group.ts +56 -42
  60. package/src/exempt.ts +50 -2
  61. package/src/findings/emitters.ts +18 -1
  62. package/src/findings/interfaces.ts +7 -1
  63. package/src/framework-lifecycle.ts +10 -6
  64. package/src/host.ts +5 -3
  65. package/src/implementations.ts +59 -23
  66. package/src/interface-satisfaction.ts +30 -27
  67. package/src/intra-function-parts.ts +43 -25
  68. package/src/inventory.ts +97 -4
  69. package/src/json-chunks.ts +87 -0
  70. package/src/partial.ts +40 -0
  71. package/src/query.ts +522 -0
  72. package/src/references.ts +119 -38
  73. package/src/reflective-lookup.ts +21 -11
  74. package/src/report.ts +14 -2
  75. package/src/reporters/annotations.ts +23 -21
  76. package/src/reporters/json.ts +3 -2
  77. package/src/reporters/reporter.ts +2 -2
  78. package/src/reporters/sarif.ts +3 -2
  79. package/src/reporters/template.ts +53 -11
  80. package/src/reporters/text.ts +12 -12
  81. package/src/roots.ts +34 -12
  82. package/src/schema.ts +2 -2
  83. package/src/serialization-contract.ts +12 -10
  84. package/src/session.ts +146 -58
  85. package/src/suppress-file.ts +6 -2
  86. package/src/sweep.ts +9 -4
  87. package/src/value-reach.ts +20 -23
  88. package/src/verbs/analyze.ts +25 -3
  89. package/src/verbs/explain.ts +20 -9
  90. package/src/verbs/print-projects.ts +54 -8
  91. package/src/verbs/print-retained.ts +6 -2
  92. package/src/verbs/print-roots.ts +5 -1
@@ -1,3 +1,4 @@
1
+ import { coalesced } from "../json-chunks.js";
1
2
  /** The kind token and the confidence a stale suppression is rendered with, both fixed. */
2
3
  const STALE_KIND = "suppression";
3
4
  const STALE_CONFIDENCE = "certain";
@@ -24,11 +25,12 @@ function summary(totals) {
24
25
  * summary is deliberately not in that shape, so a filter for finding lines yields exactly
25
26
  * them. Nothing else is written: no timestamp, duration or host detail.
26
27
  */
27
- export const text = (report) => {
28
- const lines = report.findings.map((found) => `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})`);
28
+ export const text = (report) => coalesced((function* lines() {
29
+ for (const found of report.findings) {
30
+ yield `${found.position.path}:${String(found.position.line)}:${String(found.position.column)}: ${found.symbol.kind} ${found.symbol.name}: ${found.message} [${found.confidence}] (${found.code})\n`;
31
+ }
29
32
  for (const stale of report.stale_suppressions) {
30
- lines.push(`${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})`);
33
+ yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
31
34
  }
32
- lines.push(summary(report.totals));
33
- return `${lines.join("\n")}\n`;
34
- };
35
+ yield `${summary(report.totals)}\n`;
36
+ })());
package/dist/src/roots.js CHANGED
@@ -12,6 +12,7 @@ import { entryPoints } from "./entry-points.js";
12
12
  import { globExpression } from "./glob.js";
13
13
  import { nodeKey } from "./inventory.js";
14
14
  import { byPosition } from "./position.js";
15
+ import { UNANSWERED } from "./query.js";
15
16
  import { sourceFilesOf } from "./source-files.js";
16
17
  /** Whether a configured string carries either special character. */
17
18
  function isPattern(text) {
@@ -142,7 +143,7 @@ export function roots(project, held, targetRoot, options) {
142
143
  for (const point of entryPoints(project, files, options.testFiles)) {
143
144
  enter(point.file, point.rule, point.source, point.exports);
144
145
  }
145
- const exportsOf = exportTables(project, held, [
146
+ const { tables: exportsOf, answered } = exportTables(project, held, [
146
147
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
147
148
  ...published,
148
149
  ]);
@@ -171,41 +172,56 @@ export function roots(project, held, targetRoot, options) {
171
172
  const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
172
173
  return site || compare(a.kind, b.kind) || compare(a.source, b.source);
173
174
  });
174
- return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
175
+ return {
176
+ configFile: project.configFile,
177
+ liveUnderReachability: ordered,
178
+ unmatched,
179
+ unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
180
+ };
175
181
  }
176
182
  /**
177
183
  * Every declaration each file's module exports, by the file's name: the declaration
178
184
  * each name of the export table is, and every link of a re-export's chain. A name
179
- * whose declaration is outside the project's own files belongs to another program
180
- * and names nothing here.
181
- *
182
- * One batch resolves every file to its module and one request per module reads
183
- * its export table, star re-exports resolved into it. A file that is no module
184
- * exports nothing.
185
+ * whose declaration is outside the project's own files names nothing here. One batch
186
+ * resolves every file to its module and one request per module reads its export
187
+ * table, star re-exports resolved into it. A file that is no module exports nothing.
188
+ * `answered` is false where the checker left a table or a link of a chain unanswered.
185
189
  */
186
190
  function exportTables(project, held, wanted) {
187
191
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
188
192
  const tables = new Map();
193
+ let answered = true;
189
194
  if (unique.length === 0) {
190
- return tables;
195
+ return { tables, answered };
191
196
  }
192
197
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
193
198
  const chains = aliasChains(project, held);
199
+ const topLevel = (file) => {
200
+ const fileId = held.declarations.get(nodeKey(file, file));
201
+ return held.symbols.filter((symbol) => symbol.parent === fileId).map((symbol) => symbol.id);
202
+ };
194
203
  unique.forEach((file, index) => {
195
204
  const module = modules[index];
196
205
  if (module === undefined) {
197
206
  tables.set(file.fileName, []);
198
207
  return;
199
208
  }
209
+ const table = module === UNANSWERED ? UNANSWERED : project.queries.exportsOfModule(module);
210
+ if (table === UNANSWERED) {
211
+ answered = false;
212
+ tables.set(file.fileName, topLevel(file));
213
+ return;
214
+ }
200
215
  const ids = new Set();
201
- for (const exported of project.checker.getExportsOfModule(module)) {
216
+ for (const exported of table) {
202
217
  for (const target of chains.chainOf(exported)) {
218
+ answered &&= !target.guessed;
203
219
  ids.add(target.id);
204
220
  }
205
221
  }
206
222
  tables.set(file.fileName, [...ids]);
207
223
  });
208
- return tables;
224
+ return { tables, answered };
209
225
  }
210
226
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
211
227
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
@@ -218,10 +218,10 @@ export function unimplementedKey(label, path) {
218
218
  const nearest = nearestKey(path);
219
219
  let hint = "";
220
220
  if (nearest === path) {
221
- hint = `; ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
221
+ hint = `. ${JSON.stringify(path)} is one setting written as nested objects, not as one key`;
222
222
  }
223
223
  else if (nearest !== "") {
224
- hint = `; the nearest implemented key is ${JSON.stringify(nearest)}`;
224
+ hint = `. The nearest implemented key is ${JSON.stringify(nearest)}`;
225
225
  }
226
226
  return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented${hint}`);
227
227
  }
@@ -16,6 +16,7 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
16
16
  import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
17
17
  import { nodeKey } from "./inventory.js";
18
18
  import { renderPosition } from "./position.js";
19
+ import { must } from "./query.js";
19
20
  import { valueReach } from "./value-reach.js";
20
21
  /** The serializer every run reads, whatever the configuration names. */
21
22
  const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
@@ -185,12 +186,13 @@ function erasedParameters(project, parameters) {
185
186
  if (!parameter.rest) {
186
187
  return (type.flags & ERASED) !== 0;
187
188
  }
188
- const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
189
+ const [element] = must(project.queries.typeArguments(type));
189
190
  return element !== undefined && (element.flags & ERASED) !== 0;
190
191
  });
191
192
  const bySymbol = new Map();
192
193
  project
193
194
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
195
+ .map(must)
194
196
  .forEach((symbol, index) => {
195
197
  const parameter = erased[index];
196
198
  if (symbol !== undefined && parameter !== undefined) {
@@ -206,11 +208,11 @@ function handedOn(project, sites, erased, invocationOf) {
206
208
  .filter((value) => spellings.has(value.name.getText()))
207
209
  .map((value) => ({ site, argument: index, value }))));
208
210
  const plain = candidates.filter((one) => !one.value.shorthand);
209
- const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
211
+ const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
210
212
  const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
211
213
  return candidates.flatMap((one) => {
212
214
  const symbol = one.value.shorthand
213
- ? project.shorthandValueAt(project.handle(one.value.name))
215
+ ? must(project.shorthandValueAt(project.handle(one.value.name)))
214
216
  : symbolOf.get(one);
215
217
  const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
216
218
  return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
@@ -226,8 +228,8 @@ function handedOn(project, sites, erased, invocationOf) {
226
228
  */
227
229
  function destinations(input, chains, walked) {
228
230
  const { project, held } = input;
229
- const checker = project.checker;
230
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
231
+ const queries = project.queries;
232
+ const own = project.ownPaths();
231
233
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
232
234
  const forms = new Map();
233
235
  for (const site of walked.sites) {
@@ -259,7 +261,7 @@ function destinations(input, chains, walked) {
259
261
  if (form !== undefined &&
260
262
  method !== undefined &&
261
263
  outside(method) &&
262
- FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")) {
264
+ FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
263
265
  return form.invocation;
264
266
  }
265
267
  return { callee: call.expression, arguments: call.arguments, indirect: false };
@@ -275,14 +277,14 @@ function destinations(input, chains, walked) {
275
277
  const invoked = invocation(call);
276
278
  // Through a method of the prototype the resolved signature is the method's, so
277
279
  // the parameters are read off the callee's own signatures, any one erasing.
278
- const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
280
+ const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
279
281
  const signatures = invoked.indirect
280
282
  ? type === undefined
281
283
  ? []
282
- : checker.getSignaturesOfType(type, SignatureKind.Call)
283
- : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
284
+ : must(queries.signaturesOf(type, SignatureKind.Call))
285
+ : [must(queries.resolvedSignature(call))].filter((one) => one !== undefined);
284
286
  indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
285
- const parameter = checker.getParameterType(signature, index);
287
+ const parameter = must(queries.parameterType(signature, index));
286
288
  return parameter !== undefined && (parameter.flags & ERASED) !== 0;
287
289
  })
288
290
  ? [index]
@@ -1,6 +1,6 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
- import { isIdentifier } from "@typescript/native/unstable/ast";
3
2
  import { DiscoveryError } from "./discover.js";
3
+ import { queriesOf, UnansweredLog, } from "./query.js";
4
4
  /**
5
5
  * Opens the compiler client the analysis reads through.
6
6
  *
@@ -13,7 +13,9 @@ export function openEngine(options) {
13
13
  let closed = false;
14
14
  return {
15
15
  parseConfigFile: (file) => api.parseConfigFile(file),
16
- updateSnapshot: (openProjects) => api.updateSnapshot({ openProjects: [...openProjects] }),
16
+ createSnapshot: (openProjects) => api.createSnapshot({ openProjects: [...openProjects] }),
17
+ batch: (questions) => api.batch(...questions),
18
+ ask: (_accessor, _locations, question) => question(),
17
19
  getTimingInfo: () => api.getTimingInfo(),
18
20
  close: () => {
19
21
  if (closed) {
@@ -24,46 +26,53 @@ export function openEngine(options) {
24
26
  },
25
27
  };
26
28
  }
27
- function viewOf(project) {
29
+ function viewOf(project, configFile, engine, log) {
30
+ const queries = queriesOf(project.checker, configFile, (questions) => engine.batch(questions), log, (accessor, locations, question) => engine.ask(accessor, locations, question));
28
31
  // The snapshot is never updated, so a project's own file set is the same answer
29
32
  // every time it is asked for. It is read once because the question is not free:
30
33
  // deciding it consults the metadata of every file the program holds, the compiler's
31
34
  // own library files included, and a run asks for the set once per pass.
32
35
  let ownFiles;
36
+ let ownKeys;
37
+ const ownSourceFiles = () => {
38
+ ownFiles ??= project.program
39
+ .getSourceFileNames()
40
+ // The metadata decides which files belong to the target, and it is read by
41
+ // name. Fetching each file first and filtering afterwards costs one round
42
+ // trip per library file the answer then discards, and a program holds far
43
+ // more library files than target files.
44
+ .filter((name) => {
45
+ const metadata = project.program.getSourceFileMetadata(name);
46
+ return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
47
+ })
48
+ .map((name) => project.program.getSourceFile(name))
49
+ .filter((file) => file !== undefined);
50
+ return ownFiles;
51
+ };
33
52
  return {
34
- configFile: project.configFileName,
53
+ configFile,
35
54
  program: project.program,
36
- checker: project.checker,
37
- ownSourceFiles: () => {
38
- ownFiles ??= project.program
39
- .getSourceFileNames()
40
- // The metadata decides which files belong to the target, and it is read by
41
- // name. Fetching each file first and filtering afterwards costs one round
42
- // trip per library file the answer then discards, and a program holds far
43
- // more library files than target files.
44
- .filter((name) => {
45
- const metadata = project.program.getSourceFileMetadata(name);
46
- return (metadata !== undefined && !metadata.isDefaultLibrary && !metadata.isFromExternalLibrary);
47
- })
48
- .map((name) => project.program.getSourceFile(name))
49
- .filter((file) => file !== undefined);
50
- return ownFiles;
55
+ queries,
56
+ ownSourceFiles,
57
+ ownPaths: () => {
58
+ ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
59
+ return ownKeys;
51
60
  },
52
61
  handle: (node) => ({ node }),
53
- symbolsAt: (handles) => project.checker.getSymbolAtLocation(handles.map(({ node }) => node)),
54
- resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
55
- shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
62
+ symbolsAt: (handles, cap) => queries.symbolsAt(handles.map(({ node }) => node), cap),
63
+ resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
64
+ shorthandValueAt: ({ node }) => queries.shorthandValue(node),
56
65
  aliasStepOf: (symbol) => {
57
66
  // The step is asked of an alias only: asked of any other symbol, the compiler
58
67
  // fails an assertion and the session ends.
59
68
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
60
69
  return undefined;
61
70
  }
62
- const held = project.checker.getImmediateAliasedSymbol(symbol);
71
+ const held = queries.immediateAliased(symbol);
63
72
  // An alias that resolves to nothing answers with the checker's own unknown
64
73
  // symbol, which declares nothing; a symbol that is no alias answers with
65
74
  // nothing at all.
66
- return held === undefined || held.declarations.length === 0 ? undefined : held;
75
+ return typeof held === "object" && held.declarations.length === 0 ? undefined : held;
67
76
  },
68
77
  declarationAt: (handle) => {
69
78
  const node = handle.resolve(project);
@@ -78,11 +87,17 @@ function viewOf(project) {
78
87
  */
79
88
  export function narrowedTo(project, keep) {
80
89
  let kept;
90
+ let keys;
91
+ const ownSourceFiles = () => {
92
+ kept ??= project.ownSourceFiles().filter(keep);
93
+ return kept;
94
+ };
81
95
  return {
82
96
  ...project,
83
- ownSourceFiles: () => {
84
- kept ??= project.ownSourceFiles().filter(keep);
85
- return kept;
97
+ ownSourceFiles,
98
+ ownPaths: () => {
99
+ keys ??= new Set(ownSourceFiles().map((file) => file.path));
100
+ return keys;
86
101
  },
87
102
  };
88
103
  }
@@ -93,6 +108,14 @@ export function diagnosticsOf(project) {
93
108
  syntactic: project.program.getSyntacticDiagnostics(),
94
109
  semantic: project.program.getSemanticDiagnostics(),
95
110
  configParsing: project.program.getConfigFileParsingDiagnostics(),
111
+ locate: (fileName, offset) => {
112
+ const file = project.program.getSourceFile(fileName);
113
+ if (file === undefined || offset < 0 || offset > file.text.length) {
114
+ return undefined;
115
+ }
116
+ const { line, character } = file.getLineAndCharacterOfPosition(offset);
117
+ return { line: line + 1, column: character + 1 };
118
+ },
96
119
  };
97
120
  }
98
121
  /**
@@ -103,24 +126,41 @@ export function diagnosticsOf(project) {
103
126
  * with its own program and checker; it is never updated, because the analysis
104
127
  * reads and the tree does not change under it. The order the snapshot lists its
105
128
  * projects in is the compiler's own, so each named configuration is looked up in it
106
- * instead, and a configuration the snapshot opened no project for ends the run: the
107
- * run would otherwise answer for fewer projects than it was asked to analyze. The
108
- * snapshot is disposed once when the last project has been visited, and the client
109
- * is released after it, whether the visit completed or threw.
129
+ * instead. A configuration the snapshot opened no project for ends the run under
130
+ * `"refuse"` and is named in the result under `"record"`. The snapshot is disposed once
131
+ * the last project has been visited, and the client is released after it, whether the
132
+ * visit completed or threw.
110
133
  */
111
- export function runSession(engine, configFiles, visit) {
134
+ export function runSession(engine, configFiles, visit, unopened = "refuse") {
112
135
  try {
113
- const snapshot = engine.updateSnapshot(configFiles);
136
+ const snapshot = engine.createSnapshot(configFiles);
114
137
  try {
115
- const opened = new Map(snapshot.getProjects().map((project) => [project.configFileName, project]));
116
- const projects = configFiles.map((configFile) => {
138
+ const opened = new Map();
139
+ for (const project of snapshot.getProjects()) {
140
+ if (project.configFileName !== undefined) {
141
+ opened.set(project.configFileName, project);
142
+ }
143
+ }
144
+ const log = new UnansweredLog();
145
+ const projects = [];
146
+ const missing = [];
147
+ for (const configFile of configFiles) {
117
148
  const project = opened.get(configFile);
118
149
  if (project === undefined) {
119
- throw new DiscoveryError(`${configFile} opened no project`, []);
150
+ if (unopened === "refuse") {
151
+ throw new DiscoveryError(`${configFile} opened no project`, []);
152
+ }
153
+ missing.push(configFile);
154
+ continue;
120
155
  }
121
- return visit(viewOf(project));
122
- });
123
- return { projects, timing: engine.getTimingInfo() };
156
+ projects.push(visit(viewOf(project, configFile, engine, log)));
157
+ }
158
+ return {
159
+ projects,
160
+ unopened: missing,
161
+ timing: engine.getTimingInfo(),
162
+ unanswered: log.questions,
163
+ };
124
164
  }
125
165
  finally {
126
166
  snapshot.dispose();
@@ -4,6 +4,7 @@
4
4
  * one record shape, so one decoder and one matcher serve both, and a row moves into the
5
5
  * ignore file by gaining a maintainer's reason.
6
6
  */
7
+ import { jsonChunks } from "./json-chunks.js";
7
8
  import { joinPath } from "./paths.js";
8
9
  import { positionAt, walkDocument } from "./json-document.js";
9
10
  import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
@@ -213,5 +214,5 @@ export function writeBaseline(findings, provenance) {
213
214
  }
214
215
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
215
216
  });
216
- return `${JSON.stringify({ description: BASELINE_DESCRIPTION, baseline }, null, 2)}\n`;
217
+ return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
217
218
  }
package/dist/src/sweep.js CHANGED
@@ -16,7 +16,10 @@ import { namesACaller, OUTSIDE } from "./graph.js";
16
16
  */
17
17
  export function sweep(graph, input) {
18
18
  const marked = flagged(graph, input.marked);
19
- const exempt = flagged(graph, (input.exempt ?? []).map((record) => record.id));
19
+ const exempt = flagged(graph, [
20
+ ...(input.exempt ?? []).map((record) => record.id),
21
+ ...(input.unanswered ?? []),
22
+ ]);
20
23
  const held = marked.map((mark, at) => mark || exempt[at] === true);
21
24
  // Every root, every mark and every exemption seeds reachability. A root of a kind that
22
25
  // names a caller, and a mark, are live under reference counting as well; a root that
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
11
11
  import { classNames, typesAt } from "./class-members.js";
12
+ import { must } from "./query.js";
12
13
  /** The object types whose members the walk reads whatever declares them. */
13
14
  const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
14
15
  /** The symbols of an anonymous object type the target writes as a type. */
@@ -17,10 +18,10 @@ const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
17
18
  const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
18
19
  /** The global array types, whose element type is what an array value carries. */
19
20
  function arrayTargets(project) {
20
- const checker = project.checker;
21
+ const queries = project.queries;
21
22
  return new Set(["Array", "ReadonlyArray"].flatMap((name) => {
22
- const symbol = checker.resolveName(name, SymbolFlags.Type, undefined, false);
23
- return symbol === undefined ? [] : [checker.getDeclaredTypeOfSymbol(symbol).id];
23
+ const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
24
+ return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
24
25
  }));
25
26
  }
26
27
  /**
@@ -28,7 +29,7 @@ function arrayTargets(project) {
28
29
  * project's inventory.
29
30
  */
30
31
  export function valueReach(project, held) {
31
- const checker = project.checker;
32
+ const queries = project.queries;
32
33
  const names = [...classNames(project, held)];
33
34
  const declared = typesAt(project, names.map(([, node]) => node));
34
35
  const classOfTarget = new Map();
@@ -39,12 +40,10 @@ export function valueReach(project, held) {
39
40
  }
40
41
  });
41
42
  const arrays = arrayTargets(project);
42
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
43
+ const own = project.ownPaths();
43
44
  const children = new Map();
44
45
  /** The generic target of a type reference, and the type itself otherwise. */
45
- const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 && type.isTypeReference()
46
- ? type.getTarget()
47
- : type;
46
+ const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
48
47
  const classOf = (type) => {
49
48
  if (!type.isObjectType()) {
50
49
  return undefined;
@@ -56,7 +55,7 @@ export function valueReach(project, held) {
56
55
  if ((type.objectFlags & WRITTEN) !== 0) {
57
56
  return true;
58
57
  }
59
- const symbol = type.getSymbol();
58
+ const symbol = must(queries.symbolOfType(type));
60
59
  if (symbol === undefined) {
61
60
  return false;
62
61
  }
@@ -68,13 +67,11 @@ export function valueReach(project, held) {
68
67
  };
69
68
  /** The types a value of one object type holds in its data members. */
70
69
  const memberTypes = (type) => {
71
- const properties = checker
72
- .getPropertiesOfType(type)
73
- .filter((property) => (property.flags & SymbolFlags.Property) !== 0);
74
- const types = properties.length === 0 ? [] : checker.getTypeOfSymbol(properties);
70
+ const properties = must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0);
71
+ const types = queries.typesOfSymbols(properties).map(must);
75
72
  return [
76
73
  ...types.filter((held) => held !== undefined),
77
- ...checker.getIndexInfosOfType(type).map((info) => info.valueType),
74
+ ...must(queries.indexInfosOf(type)).map((info) => info.valueType),
78
75
  ];
79
76
  };
80
77
  /** Whether the walk reads one type's members because the target writes the type, not as a class. */
@@ -89,9 +86,7 @@ export function valueReach(project, held) {
89
86
  return !isList && isWritten(type);
90
87
  };
91
88
  /** The data members and conversion methods of one written object type. */
92
- const carried = (type) => checker
93
- .getPropertiesOfType(type)
94
- .filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
89
+ const carried = (type) => must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
95
90
  ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
96
91
  /** The types a value of one type holds, read once per type. */
97
92
  const childrenOf = (type) => {
@@ -101,7 +96,7 @@ export function valueReach(project, held) {
101
96
  }
102
97
  let found = [];
103
98
  if (type.isUnionType() || type.isIntersectionType()) {
104
- found = type.getTypes();
99
+ found = must(queries.constituents(type));
105
100
  }
106
101
  else if (type.isObjectType()) {
107
102
  const target = targetOf(type);
@@ -112,7 +107,7 @@ export function valueReach(project, held) {
112
107
  found = memberTypes(type);
113
108
  }
114
109
  else if (isList && type.isTypeReference()) {
115
- found = checker.getTypeArguments(type);
110
+ found = must(queries.typeArguments(type));
116
111
  }
117
112
  }
118
113
  children.set(type.id, found);
@@ -4,6 +4,7 @@ import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
5
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
+ import { partialNotes } from "../partial.js";
7
8
  import { buildReport, capFindings, sortFindings, } from "../report.js";
8
9
  import { json } from "../reporters/json.js";
9
10
  import { RENDERINGS } from "../reporters/reporters.js";
@@ -61,6 +62,16 @@ function runRelative(invoked, path) {
61
62
  }
62
63
  return below;
63
64
  }
65
+ /** Each project the run dropped as a report names it, by its configuration file below the target. */
66
+ function notBuiltOf(dropped, targetRoot) {
67
+ return dropped.map((one) => {
68
+ const project = relativePath(targetRoot, one.configFile);
69
+ if (project === undefined) {
70
+ throw new ReportPathError(`the project ${one.id} would be opened from ${one.configFile}, which is not below the target ${targetRoot}, so no report can name it`);
71
+ }
72
+ return { id: one.id, project, error: one.error };
73
+ });
74
+ }
64
75
  /** Each project of the run as a report names it, by its configuration file below the target. */
65
76
  function configurationsOf(projects, targetRoot) {
66
77
  return projects.map((one) => {
@@ -148,6 +159,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
148
159
  let report;
149
160
  let recorded;
150
161
  let verdict;
162
+ let partial;
151
163
  try {
152
164
  const root = runRelative(invoked, targetRoot);
153
165
  const mode = { production: true };
@@ -163,6 +175,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
163
175
  recorded = write.rows;
164
176
  }
165
177
  const { result, run } = analysis;
178
+ partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
166
179
  const built = buildReport({
167
180
  contractVersion: CONTRACT_VERSION,
168
181
  version,
@@ -170,6 +183,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
170
183
  declaredGaps: DECLARED_GAPS,
171
184
  target: { kind, root, identity: moduleIdentity(host, scoped.target) },
172
185
  configurations: configurationsOf(run.projects, targetRoot),
186
+ notBuilt: notBuiltOf(run.notBuilt, targetRoot),
173
187
  loaded: run.consumers.map((consumer) => ({
174
188
  id: consumer.id,
175
189
  path: runRelative(invoked, consumer.path),
@@ -189,7 +203,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
189
203
  return EXIT_FAILURE;
190
204
  }
191
205
  try {
192
- host.writeFile(documents.report, json(report));
206
+ host.writeDocument(documents.report, json(report));
193
207
  const options = {
194
208
  failOn: config.reporters.failOn,
195
209
  readSource: (path) => host.readFile(joinPath(targetRoot, path)),
@@ -198,17 +212,20 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
198
212
  for (const format of documents.formats) {
199
213
  const rendering = RENDERINGS.get(format);
200
214
  if (rendering !== undefined) {
201
- host.writeFile(documents.report + rendering.suffix, rendering.render(report, options));
215
+ host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
202
216
  }
203
217
  }
204
218
  if (documents.baseline !== undefined) {
205
- host.writeFile(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
219
+ host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
206
220
  }
207
221
  }
208
222
  catch (error) {
209
223
  err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
210
224
  return EXIT_FAILURE;
211
225
  }
226
+ for (const note of partial) {
227
+ err.write(`deadset-ts: ${note}\n`);
228
+ }
212
229
  if (report.totals.pending > 0) {
213
230
  err.write(`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`);
214
231
  }