@cplieger/deadset-ts 3.12.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
@@ -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();
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The function a declaration of the inventory declares, where it declares one: the node
3
+ * whose parameters, body and result are the declaration's own parts.
4
+ */
5
+ import { isArrowFunction, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isMethodDeclaration, isPropertyDeclaration, isVariableDeclaration, } from "@typescript/native/unstable/ast";
6
+ /** Whether one node is a function value written in place: an arrow or a function expression. */
7
+ function isFunctionValue(node) {
8
+ return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
9
+ }
10
+ /**
11
+ * The function one declaration declares: a function declaration and a class method with
12
+ * a body are their own; a variable and a class property whose initializer is an arrow or
13
+ * a function expression declare that function. Any other declaration declares none, an
14
+ * overload signature and an abstract or ambient method among them, because no body is
15
+ * there to read its parts.
16
+ */
17
+ export function signatureOf(node) {
18
+ if (isFunctionDeclaration(node)) {
19
+ return node.body === undefined ? undefined : node;
20
+ }
21
+ if (isMethodDeclaration(node)) {
22
+ const owner = node.parent;
23
+ return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
24
+ ? node
25
+ : undefined;
26
+ }
27
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
28
+ isFunctionValue(node.initializer)) {
29
+ return node.initializer;
30
+ }
31
+ return undefined;
32
+ }
@@ -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";
@@ -105,6 +106,14 @@ function readRecord(value, site, byReference, shape) {
105
106
  }
106
107
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
107
108
  }
109
+ function byReferenceOf(symbols) {
110
+ const byReference = new Map();
111
+ for (const symbol of symbols) {
112
+ const key = referenceKey(symbol.ref, symbol.position.path);
113
+ byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
114
+ }
115
+ return byReference;
116
+ }
108
117
  /** Reads one document at the target root, binding each record against the run's declarations. */
109
118
  function readDocument(host, targetRoot, symbols, shape) {
110
119
  const file = joinPath(targetRoot, shape.file);
@@ -139,11 +148,7 @@ function readDocument(host, targetRoot, symbols, shape) {
139
148
  if ((description !== undefined && typeof description !== "string") || !Array.isArray(held)) {
140
149
  return malformed(shape.file);
141
150
  }
142
- const byReference = new Map();
143
- for (const symbol of symbols) {
144
- const key = referenceKey(symbol.ref, symbol.position.path);
145
- byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
146
- }
151
+ const byReference = byReferenceOf(symbols);
147
152
  const records = [];
148
153
  const refusals = [];
149
154
  held.forEach((value, index) => {
@@ -172,9 +177,26 @@ export function readIgnoreFile(host, targetRoot, symbols) {
172
177
  export function readBaseline(host, targetRoot, symbols) {
173
178
  return readDocument(host, targetRoot, symbols, BASELINE_SHAPE);
174
179
  }
180
+ /**
181
+ * Rows a baseline write holds in memory, bound against the run's declarations as the same
182
+ * rows read back from the document would be, each carrying the reason given.
183
+ */
184
+ export function bindRows(rows, reason, symbols) {
185
+ const byReference = byReferenceOf(symbols);
186
+ const site = documentSite(BASELINE_SHAPE);
187
+ const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
188
+ return {
189
+ records: read.flatMap((one) => one.records),
190
+ refusals: read.flatMap((one) => one.refusals),
191
+ };
192
+ }
175
193
  /** What a written document says about itself, so the same report writes the same bytes. */
176
194
  const BASELINE_DESCRIPTION = "Recorded findings: a run fails only on a finding this document does not hold. " +
177
195
  "Every reason is the analyzer that recorded the row, never an adjudication.";
196
+ /** The reason every row a baseline write records carries. */
197
+ export function rowReason(provenance) {
198
+ return `recorded by ${provenance.analyzer} ${provenance.version}`;
199
+ }
178
200
  /**
179
201
  * A baseline holding one row per finding, in the order given, which is the report's. Every
180
202
  * row carries the provenance as its reason. A finding missing a value a row needs, and an
@@ -185,12 +207,12 @@ export function writeBaseline(findings, provenance) {
185
207
  if (provenance.analyzer === "" || provenance.version === "") {
186
208
  throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
187
209
  }
188
- const reason = `recorded by ${provenance.analyzer} ${provenance.version}`;
210
+ const reason = rowReason(provenance);
189
211
  const baseline = findings.map((found) => {
190
212
  if (found.code === "" || found.symbol === "" || found.path === "") {
191
213
  throw new Error(`a baseline row would carry no reason: a finding names code ${JSON.stringify(found.code)}, symbol ${JSON.stringify(found.symbol)} and path ${JSON.stringify(found.path)}`);
192
214
  }
193
215
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
194
216
  });
195
- return `${JSON.stringify({ description: BASELINE_DESCRIPTION, baseline }, null, 2)}\n`;
217
+ return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
196
218
  }
@@ -6,6 +6,7 @@
6
6
  import { SyntaxKind } from "@typescript/native/unstable/ast";
7
7
  import { nodeKey } from "./inventory.js";
8
8
  import { byPosition, positionKey, renderPosition } from "./position.js";
9
+ import { signatureOf } from "./signatures.js";
9
10
  import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
10
11
  /**
11
12
  * The three expressions the grammar applies to the text of each line comment, in this
@@ -134,7 +135,8 @@ export function lineComments(file) {
134
135
  /**
135
136
  * The declarations of one file by the line each begins on, the line of its first token: a
136
137
  * decorator, a modifier or the keyword. A variable's first token is its statement's
137
- * keyword, and a variable written on a later line of the statement also begins there.
138
+ * keyword, and a variable written on a later line of the statement also begins there. A
139
+ * function also begins, for a directive, on the line of each of its parameters.
138
140
  */
139
141
  function declarationLines(file, held, symbols) {
140
142
  const below = new Map();
@@ -151,6 +153,11 @@ function declarationLines(file, held, symbols) {
151
153
  const one = id === undefined ? undefined : symbols.get(id);
152
154
  if (one !== undefined) {
153
155
  add(node.pos, one);
156
+ // A parameter on a later line of a wrapped signature takes its directive on the
157
+ // line above it, inside the parameter list, and the directive binds the function.
158
+ for (const parameter of signatureOf(node)?.parameters ?? []) {
159
+ add(parameter.pos, one);
160
+ }
154
161
  const list = node.parent;
155
162
  if (node.kind === SyntaxKind.VariableDeclaration &&
156
163
  list.kind === SyntaxKind.VariableDeclarationList &&
@@ -79,6 +79,7 @@ const ROW_SUBJECTS = new Set([
79
79
  "dependency",
80
80
  "module-directive",
81
81
  "root",
82
+ "configured-declaration",
82
83
  "suppression",
83
84
  "edge",
84
85
  ]);
@@ -86,6 +87,32 @@ const ROW_SUBJECTS = new Set([
86
87
  export function isRowSubject(kind) {
87
88
  return ROW_SUBJECTS.has(kind);
88
89
  }
90
+ /**
91
+ * The subject kinds that are a part of a declaration rather than a declaration: a part
92
+ * has no reference of its own, so a finding about one names the declaration that holds
93
+ * it, and the part is decided inside that declaration whatever uses it.
94
+ */
95
+ const PART_SUBJECTS = new Set([
96
+ "parameter",
97
+ "receiver",
98
+ "result",
99
+ "statement",
100
+ "case",
101
+ "store",
102
+ ]);
103
+ /** Whether a finding's subject is a part of the declaration its reference names. */
104
+ export function isPartSubject(kind) {
105
+ return PART_SUBJECTS.has(kind);
106
+ }
107
+ /** The prefix of the codes of the part kinds, the intra-function range. */
108
+ const PART_KIND_PREFIX = "DS18";
109
+ /**
110
+ * Whether a code is a part kind, whose record withholds its finding and marks nothing:
111
+ * the record says the part is wanted, not that the declaration holding it is.
112
+ */
113
+ export function isPartKind(code) {
114
+ return code.startsWith(PART_KIND_PREFIX);
115
+ }
89
116
  /** One finding's identity: its code and the position of the thing it names. */
90
117
  function claimKey(finding) {
91
118
  return `${finding.code}\u0000${positionKey(finding.position)}`;
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
@@ -1,22 +1,27 @@
1
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.
2
+ * The classes of the target a value of one type carries, and the members of the other
3
+ * object types the target writes that it carries, as a serializer walking the value
4
+ * reaches them: through union and intersection constituents, array and tuple elements,
5
+ * and the data members and index signatures of the target's classes and of the
6
+ * interfaces and object types the target writes. The walk stops at `any`, `unknown`, a
7
+ * type parameter, a function and an interface or class of another program, so an
8
+ * instance held only there is missed.
7
9
  */
8
10
  import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
9
11
  import { classNames, typesAt } from "./class-members.js";
12
+ import { must } from "./query.js";
10
13
  /** The object types whose members the walk reads whatever declares them. */
11
14
  const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
12
15
  /** The symbols of an anonymous object type the target writes as a type. */
13
16
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
17
+ /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
18
+ const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
14
19
  /** The global array types, whose element type is what an array value carries. */
15
20
  function arrayTargets(project) {
16
- const checker = project.checker;
21
+ const queries = project.queries;
17
22
  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];
23
+ const symbol = must(queries.resolveName(name, SymbolFlags.Type, undefined, false));
24
+ return symbol === undefined ? [] : [must(queries.declaredTypeOf(symbol)).id];
20
25
  }));
21
26
  }
22
27
  /**
@@ -24,7 +29,7 @@ function arrayTargets(project) {
24
29
  * project's inventory.
25
30
  */
26
31
  export function valueReach(project, held) {
27
- const checker = project.checker;
32
+ const queries = project.queries;
28
33
  const names = [...classNames(project, held)];
29
34
  const declared = typesAt(project, names.map(([, node]) => node));
30
35
  const classOfTarget = new Map();
@@ -35,12 +40,10 @@ export function valueReach(project, held) {
35
40
  }
36
41
  });
37
42
  const arrays = arrayTargets(project);
38
- const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
43
+ const own = project.ownPaths();
39
44
  const children = new Map();
40
45
  /** 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;
46
+ const targetOf = (type) => (type.objectFlags & ObjectFlags.Reference) !== 0 ? must(queries.targetOf(type)) : type;
44
47
  const classOf = (type) => {
45
48
  if (!type.isObjectType()) {
46
49
  return undefined;
@@ -52,7 +55,7 @@ export function valueReach(project, held) {
52
55
  if ((type.objectFlags & WRITTEN) !== 0) {
53
56
  return true;
54
57
  }
55
- const symbol = type.getSymbol();
58
+ const symbol = must(queries.symbolOfType(type));
56
59
  if (symbol === undefined) {
57
60
  return false;
58
61
  }
@@ -64,15 +67,27 @@ export function valueReach(project, held) {
64
67
  };
65
68
  /** The types a value of one object type holds in its data members. */
66
69
  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);
70
+ const properties = must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0);
71
+ const types = queries.typesOfSymbols(properties).map(must);
71
72
  return [
72
73
  ...types.filter((held) => held !== undefined),
73
- ...checker.getIndexInfosOfType(type).map((info) => info.valueType),
74
+ ...must(queries.indexInfosOf(type)).map((info) => info.valueType),
74
75
  ];
75
76
  };
77
+ /** Whether the walk reads one type's members because the target writes the type, not as a class. */
78
+ const writtenObject = (type) => {
79
+ if (!type.isObjectType() || classOf(type) !== undefined) {
80
+ return false;
81
+ }
82
+ const target = targetOf(type);
83
+ const isList = type.isTypeReference() &&
84
+ (arrays.has(target.id) ||
85
+ (target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
86
+ return !isList && isWritten(type);
87
+ };
88
+ /** The data members and conversion methods of one written object type. */
89
+ const carried = (type) => must(queries.propertiesOf(type)).filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
90
+ ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
76
91
  /** The types a value of one type holds, read once per type. */
77
92
  const childrenOf = (type) => {
78
93
  const known = children.get(type.id);
@@ -81,7 +96,7 @@ export function valueReach(project, held) {
81
96
  }
82
97
  let found = [];
83
98
  if (type.isUnionType() || type.isIntersectionType()) {
84
- found = type.getTypes();
99
+ found = must(queries.constituents(type));
85
100
  }
86
101
  else if (type.isObjectType()) {
87
102
  const target = targetOf(type);
@@ -92,15 +107,16 @@ export function valueReach(project, held) {
92
107
  found = memberTypes(type);
93
108
  }
94
109
  else if (isList && type.isTypeReference()) {
95
- found = checker.getTypeArguments(type);
110
+ found = must(queries.typeArguments(type));
96
111
  }
97
112
  }
98
113
  children.set(type.id, found);
99
114
  return found;
100
115
  };
101
116
  return {
102
- classesOf: (type) => {
103
- const found = new Set();
117
+ reachedFrom: (type) => {
118
+ const classes = new Set();
119
+ const members = [];
104
120
  const seen = new Set();
105
121
  const pending = [type];
106
122
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
@@ -110,11 +126,14 @@ export function valueReach(project, held) {
110
126
  seen.add(next.id);
111
127
  const owner = classOf(next);
112
128
  if (owner !== undefined) {
113
- found.add(owner);
129
+ classes.add(owner);
130
+ }
131
+ else if (writtenObject(next)) {
132
+ members.push(...carried(next));
114
133
  }
115
134
  pending.push(...childrenOf(next));
116
135
  }
117
- return found;
136
+ return { classes, members };
118
137
  },
119
138
  };
120
139
  }
@@ -1,15 +1,16 @@
1
- import { runAnalysis } from "../analysis.js";
1
+ import { runAnalysis, runBaselineWrite } from "../analysis.js";
2
2
  import { ConfigError } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
5
- import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.js";
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";
10
11
  import { parseTemplate, TemplateError } from "../reporters/template.js";
11
12
  import { resolve } from "../resolve.js";
12
- import { writeBaseline } from "../suppress-file.js";
13
+ import { rowReason, writeBaseline } from "../suppress-file.js";
13
14
  import { CONTRACT_VERSION } from "../version.js";
14
15
  import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
15
16
  /** The options `analyze` takes beside the ones every verb takes. */
@@ -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) => {
@@ -107,8 +118,9 @@ function templateOf(host, invoked, named, formats) {
107
118
  /**
108
119
  * Analyzes the scope under the configuration and writes the JSON report to the path
109
120
  * `--report` names, each rendering `--format` names beside it at the report's path with the
110
- * format's suffix appended, and a baseline of every finding where `--baseline-write` names a
111
- * path. The template format renders the template `--template` names. Nothing is written to
121
+ * format's suffix appended, and where `--baseline-write` names a path the baseline a fixpoint
122
+ * over the analysis records, the report then being its first round, which reads no
123
+ * baseline. The template format renders the template `--template` names. Nothing is written to
112
124
  * the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
113
125
  * under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
114
126
  */
@@ -147,11 +159,23 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
147
159
  let report;
148
160
  let recorded;
149
161
  let verdict;
162
+ let partial;
150
163
  try {
151
164
  const root = runRelative(invoked, targetRoot);
152
- const { result, run } = runAnalysis(openClient(false), host, scoped, config, provenance, {
153
- production: true,
154
- });
165
+ const mode = { production: true };
166
+ let analysis;
167
+ if (documents.baseline === undefined) {
168
+ analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
169
+ recorded = [];
170
+ }
171
+ else {
172
+ const reason = rowReason(analyzerProvenance(version));
173
+ const write = runBaselineWrite(openClient(false), host, scoped, config, provenance, mode, reason);
174
+ analysis = write.first;
175
+ recorded = write.rows;
176
+ }
177
+ const { result, run } = analysis;
178
+ partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
155
179
  const built = buildReport({
156
180
  contractVersion: CONTRACT_VERSION,
157
181
  version,
@@ -159,6 +183,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
159
183
  declaredGaps: DECLARED_GAPS,
160
184
  target: { kind, root, identity: moduleIdentity(host, scoped.target) },
161
185
  configurations: configurationsOf(run.projects, targetRoot),
186
+ notBuilt: notBuiltOf(run.notBuilt, targetRoot),
162
187
  loaded: run.consumers.map((consumer) => ({
163
188
  id: consumer.id,
164
189
  path: runRelative(invoked, consumer.path),
@@ -167,9 +192,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
167
192
  result,
168
193
  testFileRules: run.testFileRules,
169
194
  });
170
- // The baseline records every finding, so its rows are taken before the cap bounds what
171
- // a rendering prints: a baseline missing a reported finding fails the next run on it.
172
- recorded = recordedFindings(result);
173
195
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
174
196
  verdict = verdictOf(result, config);
175
197
  }
@@ -181,7 +203,7 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
181
203
  return EXIT_FAILURE;
182
204
  }
183
205
  try {
184
- host.writeFile(documents.report, json(report));
206
+ host.writeDocument(documents.report, json(report));
185
207
  const options = {
186
208
  failOn: config.reporters.failOn,
187
209
  readSource: (path) => host.readFile(joinPath(targetRoot, path)),
@@ -190,17 +212,20 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
190
212
  for (const format of documents.formats) {
191
213
  const rendering = RENDERINGS.get(format);
192
214
  if (rendering !== undefined) {
193
- host.writeFile(documents.report + rendering.suffix, rendering.render(report, options));
215
+ host.writeDocument(documents.report + rendering.suffix, rendering.render(report, options));
194
216
  }
195
217
  }
196
218
  if (documents.baseline !== undefined) {
197
- host.writeFile(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
219
+ host.writeDocument(documents.baseline, writeBaseline(recorded, analyzerProvenance(version)));
198
220
  }
199
221
  }
200
222
  catch (error) {
201
223
  err.write(`deadset-ts: ${error instanceof Error ? error.message : String(error)}\n`);
202
224
  return EXIT_FAILURE;
203
225
  }
226
+ for (const note of partial) {
227
+ err.write(`deadset-ts: ${note}\n`);
228
+ }
204
229
  if (report.totals.pending > 0) {
205
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`);
206
231
  }