@cplieger/deadset-ts 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/analysis.js +8 -1
- package/dist/src/calls.js +154 -0
- package/dist/src/class-members.js +92 -0
- package/dist/src/config.js +3 -0
- package/dist/src/configured-declarations.js +232 -0
- package/dist/src/decorator.js +3 -10
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -0
- package/dist/src/findings/emitter.js +1 -0
- package/dist/src/findings/emitters.js +19 -0
- package/dist/src/findings/interfaces.js +2 -0
- package/dist/src/findings/intra-function.js +2 -0
- package/dist/src/findings/non-code-artifacts.js +2 -0
- package/dist/src/findings/reads-and-writes.js +2 -0
- package/dist/src/findings/self-check.js +2 -0
- package/dist/src/findings/unused-declarations.js +2 -0
- package/dist/src/findings/visibility-narrowing.js +2 -0
- package/dist/src/framework-lifecycle.js +192 -0
- package/dist/src/injection-container.js +101 -0
- package/dist/src/print.js +18 -0
- package/dist/src/resolve.js +98 -0
- package/dist/src/run.js +60 -176
- package/dist/src/schema.js +18 -1
- package/dist/src/serialization-contract.js +315 -0
- package/dist/src/value-reach.js +120 -0
- package/dist/src/verbs/print-config.js +9 -0
- package/dist/src/verbs/print-projects.js +34 -0
- package/dist/src/verbs/print-retained.js +23 -0
- package/dist/src/verbs/print-roots.js +36 -0
- package/dist/src/verbs/verb.js +5 -0
- package/dist/src/verbs/version.js +7 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +8 -1
- package/src/calls.ts +224 -0
- package/src/class-members.ts +136 -0
- package/src/config.ts +31 -0
- package/src/configured-declarations.ts +308 -0
- package/src/decorator.ts +4 -16
- package/src/exempt.ts +5 -1
- package/src/findings/dependencies-and-module-machinery.ts +4 -0
- package/src/findings/emitter.ts +19 -0
- package/src/findings/emitters.ts +21 -0
- package/src/findings/interfaces.ts +4 -0
- package/src/findings/intra-function.ts +4 -0
- package/src/findings/non-code-artifacts.ts +4 -0
- package/src/findings/reads-and-writes.ts +4 -0
- package/src/findings/self-check.ts +4 -0
- package/src/findings/unused-declarations.ts +4 -0
- package/src/findings/visibility-narrowing.ts +4 -0
- package/src/framework-lifecycle.ts +280 -0
- package/src/injection-container.ts +141 -0
- package/src/print.ts +20 -1
- package/src/resolve.ts +155 -0
- package/src/run.ts +64 -200
- package/src/schema.ts +19 -1
- package/src/serialization-contract.ts +492 -0
- package/src/value-reach.ts +152 -0
- package/src/verbs/print-config.ts +10 -0
- package/src/verbs/print-projects.ts +35 -0
- package/src/verbs/print-retained.ts +24 -0
- package/src/verbs/print-roots.ts +38 -0
- package/src/verbs/verb.ts +38 -0
- package/src/verbs/version.ts +8 -0
- package/src/version.ts +1 -1
package/dist/src/run.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import { runRoots, runSweep } from "./analysis.js";
|
|
2
1
|
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { DiscoveryError, renderDiagnostic } from "./discover.js";
|
|
3
|
+
import { EMITTERS } from "./findings/emitters.js";
|
|
5
4
|
import { joinPath, resolvePath } from "./paths.js";
|
|
6
|
-
import { printConfig } from "./print.js";
|
|
7
|
-
import { resolve, resolveMatrix } from "./resolve.js";
|
|
8
5
|
import { readScope, ScopeError, scopeForDir } from "./scope.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { openEngine } from "./session.js";
|
|
7
|
+
import { printConfigVerb } from "./verbs/print-config.js";
|
|
8
|
+
import { printProjectsVerb } from "./verbs/print-projects.js";
|
|
9
|
+
import { printRetainedVerb } from "./verbs/print-retained.js";
|
|
10
|
+
import { printRootsVerb } from "./verbs/print-roots.js";
|
|
11
|
+
import { EXIT_FAILURE, EXIT_USAGE } from "./verbs/verb.js";
|
|
12
|
+
import { versionVerb } from "./verbs/version.js";
|
|
13
|
+
/**
|
|
14
|
+
* Every verb this command answers, in the order the usage text lists them. A verb
|
|
15
|
+
* with no `run` is listed, and invoking it is a usage error.
|
|
16
|
+
*/
|
|
16
17
|
const VERBS = [
|
|
17
|
-
"analyze",
|
|
18
|
-
"explain",
|
|
19
|
-
"print-config",
|
|
20
|
-
"print-projects",
|
|
21
|
-
"print-roots",
|
|
22
|
-
"print-retained",
|
|
23
|
-
"describe",
|
|
24
|
-
"version",
|
|
18
|
+
{ name: "analyze" },
|
|
19
|
+
{ name: "explain" },
|
|
20
|
+
{ name: "print-config", run: printConfigVerb },
|
|
21
|
+
{ name: "print-projects", run: printProjectsVerb },
|
|
22
|
+
{ name: "print-roots", run: printRootsVerb },
|
|
23
|
+
{ name: "print-retained", run: printRetainedVerb },
|
|
24
|
+
{ name: "describe" },
|
|
25
|
+
{ name: "version", run: versionVerb },
|
|
25
26
|
];
|
|
26
|
-
const USAGE = `usage: deadset-ts <verb> [options]\nverbs: ${VERBS.join(", ")}\n`;
|
|
27
|
+
const USAGE = `usage: deadset-ts <verb> [options]\nverbs: ${VERBS.map((verb) => verb.name).join(", ")}\n`;
|
|
27
28
|
/**
|
|
28
29
|
* The tokens whose presence in an option's name makes that option a request to
|
|
29
30
|
* edit a source file. The set is closed, and a token is matched in any position of
|
|
@@ -31,15 +32,11 @@ const USAGE = `usage: deadset-ts <verb> [options]\nverbs: ${VERBS.join(", ")}\n`
|
|
|
31
32
|
*/
|
|
32
33
|
const SOURCE_EDIT_TOKENS = ["fix", "edit", "delete", "rewrite"];
|
|
33
34
|
/**
|
|
34
|
-
* Maps an option name to the setting
|
|
35
|
-
* option, supplies the document resolution reads, and gives
|
|
36
|
-
* setting's provenance names.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* command line, and its path is the setting of the Contract's configuration schema
|
|
40
|
-
* that option supplies. The two are mapped here and never derived from one
|
|
41
|
-
* another, so this table is the authority for both: a setting reaches the command
|
|
42
|
-
* line only by an entry, and a path names a key the schema declares.
|
|
35
|
+
* Maps an option name to the setting of the Contract's configuration schema it supplies,
|
|
36
|
+
* so one entry registers the option, supplies the document resolution reads, and gives
|
|
37
|
+
* the spelling the setting's provenance names. The name is this command's vocabulary and
|
|
38
|
+
* is never derived from the path: a setting reaches the command line only by an entry,
|
|
39
|
+
* and a path names a key the schema declares.
|
|
43
40
|
*/
|
|
44
41
|
export const SETTING_OPTIONS = new Map([
|
|
45
42
|
["min-confidence", "analysis.min_confidence"],
|
|
@@ -139,20 +136,13 @@ function scopeOf(host, options) {
|
|
|
139
136
|
return scopeForDir(host, options.named.get("target") ?? ".");
|
|
140
137
|
}
|
|
141
138
|
/**
|
|
142
|
-
* Runs the
|
|
143
|
-
*
|
|
144
|
-
* error or a configuration
|
|
145
|
-
* answer. It writes nothing outside `out` and `err` and never exits the
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* analyzer reports and never edits a source file.
|
|
150
|
-
*
|
|
151
|
-
* `host` is the platform the run reads, the version it reports included;
|
|
152
|
-
* `openClient` opens the compiler client a verb that reads projects needs.
|
|
153
|
-
* Both are parameters because both are the platform, which no module below this one
|
|
154
|
-
* names: the command entry binds them, and a caller that wants to watch what a run
|
|
155
|
-
* reads supplies its own.
|
|
139
|
+
* Runs the command line over `args`, the arguments after the program name, and returns
|
|
140
|
+
* the exit code: 0 for a completed verb, 1 for a finding that fails the run, 2 for a
|
|
141
|
+
* usage error, a requested source edit or a refused configuration, 3 for a failure that
|
|
142
|
+
* produced no answer. It writes nothing outside `out` and `err` and never exits the
|
|
143
|
+
* process. `host` is the platform the run reads, the version it reports included, and
|
|
144
|
+
* `openClient` opens the compiler client; a caller that watches what a run reads
|
|
145
|
+
* supplies its own of either.
|
|
156
146
|
*/
|
|
157
147
|
export function run(args, out, err, host, openClient = (collectTiming) => openEngine({ collectTiming })) {
|
|
158
148
|
const editing = sourceEditOption(args);
|
|
@@ -161,151 +151,45 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
161
151
|
err.write(USAGE);
|
|
162
152
|
return EXIT_USAGE;
|
|
163
153
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
if (verb === "print-config") {
|
|
170
|
-
return printConfigVerb(args.slice(1), out, err, host);
|
|
171
|
-
}
|
|
172
|
-
if (verb === "print-projects") {
|
|
173
|
-
return printProjectsVerb(args.slice(1), out, err, host, openClient);
|
|
174
|
-
}
|
|
175
|
-
if (verb === "print-roots") {
|
|
176
|
-
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
154
|
+
const name = args[0];
|
|
155
|
+
const verb = VERBS.find((listed) => listed.name === name);
|
|
156
|
+
if (verb?.run !== undefined) {
|
|
157
|
+
return invoke(verb.run, args.slice(1), { out, err, host }, openClient);
|
|
177
158
|
}
|
|
178
|
-
if (verb
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
if (verb !== undefined && VERBS.includes(verb)) {
|
|
182
|
-
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
159
|
+
if (verb !== undefined) {
|
|
160
|
+
err.write(`deadset-ts: ${verb.name} is not implemented\n`);
|
|
183
161
|
return EXIT_USAGE;
|
|
184
162
|
}
|
|
185
|
-
if (
|
|
186
|
-
err.write(`deadset-ts: unknown verb ${JSON.stringify(
|
|
163
|
+
if (name !== undefined) {
|
|
164
|
+
err.write(`deadset-ts: unknown verb ${JSON.stringify(name)}\n`);
|
|
187
165
|
}
|
|
188
166
|
err.write(USAGE);
|
|
189
167
|
return EXIT_USAGE;
|
|
190
168
|
}
|
|
191
|
-
/** Writes the resolved configuration with the source of every setting. */
|
|
192
|
-
function printConfigVerb(args, out, err, host) {
|
|
193
|
-
try {
|
|
194
|
-
const options = readOptions(args);
|
|
195
|
-
const { config, provenance } = resolve(inputsOf(host, options));
|
|
196
|
-
out.write(printConfig(config, provenance));
|
|
197
|
-
return EXIT_CLEAN;
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
return refuse(error, err);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
169
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* target root.
|
|
170
|
+
* Runs one verb over the arguments after its name. The options are read the first
|
|
171
|
+
* time the verb asks for what they name, and a refusal the verb throws is written
|
|
172
|
+
* and turned into its exit code once the client the verb opened is closed.
|
|
208
173
|
*/
|
|
209
|
-
function
|
|
210
|
-
let
|
|
174
|
+
function invoke(verb, args, streams, openClient) {
|
|
175
|
+
let options;
|
|
176
|
+
const read = () => (options ??= readOptions(args));
|
|
177
|
+
let opened;
|
|
211
178
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
failures.push(...errors.map(renderDiagnostic));
|
|
222
|
-
return ids.get(project.configFile) ?? project.configFile;
|
|
179
|
+
return verb({
|
|
180
|
+
...streams,
|
|
181
|
+
inputs: () => inputsOf(streams.host, read()),
|
|
182
|
+
scope: () => scopeOf(streams.host, read()),
|
|
183
|
+
openClient: (collectTiming) => {
|
|
184
|
+
opened = openClient(collectTiming);
|
|
185
|
+
return opened;
|
|
186
|
+
},
|
|
187
|
+
emitters: EMITTERS,
|
|
223
188
|
});
|
|
224
|
-
if (failures.length > 0) {
|
|
225
|
-
for (const line of failures) {
|
|
226
|
-
err.write(`${line}\n`);
|
|
227
|
-
}
|
|
228
|
-
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
229
|
-
return EXIT_FAILURE;
|
|
230
|
-
}
|
|
231
|
-
for (const id of projects) {
|
|
232
|
-
out.write(`${id}\n`);
|
|
233
|
-
}
|
|
234
|
-
return EXIT_CLEAN;
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
engine?.close();
|
|
238
|
-
return refuse(error, err);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* One root as a line: the declaration's reference, why it is a root, and the string
|
|
243
|
-
* that named it, separated by tabs. A run of several configurations adds the ones
|
|
244
|
-
* whose project holds the root as a fourth field, and writes the third present and
|
|
245
|
-
* empty where no string named the root, so every line of one run holds the same
|
|
246
|
-
* fields.
|
|
247
|
-
*/
|
|
248
|
-
function rootLine(root, several) {
|
|
249
|
-
const line = `${root.ref}\t${root.kind}`;
|
|
250
|
-
if (several) {
|
|
251
|
-
return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
|
|
252
|
-
}
|
|
253
|
-
return root.source === "" ? line : `${line}\t${root.source}`;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Writes the root set of the run, one root per line, and then names on the error
|
|
257
|
-
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
258
|
-
* one such string fails the run. The line shape is this command's own rather than a
|
|
259
|
-
* Contract format, so that sort and cut read it.
|
|
260
|
-
*/
|
|
261
|
-
function printRootsVerb(args, out, err, host, openClient) {
|
|
262
|
-
let engine;
|
|
263
|
-
try {
|
|
264
|
-
const options = readOptions(args);
|
|
265
|
-
const { config, provenance } = resolve(inputsOf(host, options));
|
|
266
|
-
const scope = scopeOf(host, options);
|
|
267
|
-
engine = openClient(false);
|
|
268
|
-
const answer = runRoots(engine, host, scope, config, provenance);
|
|
269
|
-
const several = answer.configurations.length > 1;
|
|
270
|
-
for (const root of answer.roots) {
|
|
271
|
-
out.write(`${rootLine(root, several)}\n`);
|
|
272
|
-
}
|
|
273
|
-
for (const finding of answer.findings) {
|
|
274
|
-
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
275
|
-
}
|
|
276
|
-
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
engine?.close();
|
|
280
|
-
return refuse(error, err);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Writes every declaration an exemption held back in the production sweep a report is
|
|
285
|
-
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
286
|
-
* several configurations found is one record, so the line names no configuration. The
|
|
287
|
-
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
288
|
-
* read it.
|
|
289
|
-
*/
|
|
290
|
-
function printRetainedVerb(args, out, err, host, openClient) {
|
|
291
|
-
let engine;
|
|
292
|
-
try {
|
|
293
|
-
const options = readOptions(args);
|
|
294
|
-
const { config } = resolve(inputsOf(host, options));
|
|
295
|
-
const scope = scopeOf(host, options);
|
|
296
|
-
engine = openClient(false);
|
|
297
|
-
const swept = runSweep(engine, host, scope, config, {
|
|
298
|
-
marked: [],
|
|
299
|
-
mode: { production: true },
|
|
300
|
-
});
|
|
301
|
-
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
302
|
-
out.write(`${line}\n`);
|
|
303
|
-
}
|
|
304
|
-
return EXIT_CLEAN;
|
|
305
189
|
}
|
|
306
190
|
catch (error) {
|
|
307
|
-
|
|
308
|
-
return refuse(error, err);
|
|
191
|
+
opened?.close();
|
|
192
|
+
return refuse(error, streams.err);
|
|
309
193
|
}
|
|
310
194
|
}
|
|
311
195
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
package/dist/src/schema.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ConfigError } from "./config.js";
|
|
2
2
|
const leaf = { kind: "leaf" };
|
|
3
|
+
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
4
|
+
const declarations = {
|
|
5
|
+
kind: "list",
|
|
6
|
+
members: { symbol: leaf, module: leaf, name: leaf, global: leaf },
|
|
7
|
+
};
|
|
3
8
|
/**
|
|
4
9
|
* The closed key list the Contract's configuration schema declares, derived from
|
|
5
10
|
* that schema key by key. A test pins the two equal, so a Contract change that
|
|
@@ -44,7 +49,19 @@ export const SCHEMA_ROOT = {
|
|
|
44
49
|
},
|
|
45
50
|
},
|
|
46
51
|
go: { kind: "section", members: {} },
|
|
47
|
-
ts: {
|
|
52
|
+
ts: {
|
|
53
|
+
kind: "section",
|
|
54
|
+
members: {
|
|
55
|
+
test_files: leaf,
|
|
56
|
+
entry_files: leaf,
|
|
57
|
+
injection_registrations: declarations,
|
|
58
|
+
lifecycle_contracts: {
|
|
59
|
+
kind: "list",
|
|
60
|
+
members: { components: declarations, bases: declarations, members: leaf },
|
|
61
|
+
},
|
|
62
|
+
serializers: declarations,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
48
65
|
provenance: { kind: "map" },
|
|
49
66
|
},
|
|
50
67
|
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serialization-contract class: the properties of a class whose values reach
|
|
3
|
+
* `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
|
|
4
|
+
* `toString` where a value is passed to an `unknown` or `any` parameter of a function
|
|
5
|
+
* outside the analysis. A function of the project handing such a parameter of its own
|
|
6
|
+
* to a destination is one, holding back what that destination does, to a fixpoint.
|
|
7
|
+
*/
|
|
8
|
+
import { isArrowFunction, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { aliasChains } from "./alias-chain.js";
|
|
11
|
+
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
12
|
+
import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
13
|
+
import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
14
|
+
import { nodeKey } from "./inventory.js";
|
|
15
|
+
import { renderPosition } from "./position.js";
|
|
16
|
+
import { valueReach } from "./value-reach.js";
|
|
17
|
+
/** The serializer every run reads, whatever the configuration names. */
|
|
18
|
+
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
19
|
+
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
20
|
+
const CONVERSIONS = new Set(["toJSON", "toString"]);
|
|
21
|
+
/** The parameter types that keep nothing of the value they are given. */
|
|
22
|
+
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
23
|
+
/** The wider of two reaches, where either may be absent. */
|
|
24
|
+
function wider(a, b) {
|
|
25
|
+
return a === "conversions" || b === "conversions" ? "conversions" : (a ?? b);
|
|
26
|
+
}
|
|
27
|
+
/** Whether one node declares a static member, whose `this` is the class itself. */
|
|
28
|
+
function isStatic(node) {
|
|
29
|
+
const flags = node.modifierFlags ?? 0;
|
|
30
|
+
return (flags & ModifierFlags.Static) !== 0;
|
|
31
|
+
}
|
|
32
|
+
/** Whether one node opens a body whose `this` is not an instance of the enclosing class. */
|
|
33
|
+
function resetsThis(node) {
|
|
34
|
+
return (isFunctionDeclaration(node) ||
|
|
35
|
+
isFunctionExpression(node) ||
|
|
36
|
+
isClassStaticBlockDeclaration(node) ||
|
|
37
|
+
isStatic(node) ||
|
|
38
|
+
((isMethodDeclaration(node) ||
|
|
39
|
+
isGetAccessorDeclaration(node) ||
|
|
40
|
+
isSetAccessorDeclaration(node)) &&
|
|
41
|
+
isObjectLiteralExpression(node.parent)));
|
|
42
|
+
}
|
|
43
|
+
/** The parameters and declaring node of one function-like declaration of the project. */
|
|
44
|
+
function functionOf(node) {
|
|
45
|
+
if (isFunctionDeclaration(node) ||
|
|
46
|
+
(isMethodDeclaration(node) && !isObjectLiteralExpression(node.parent))) {
|
|
47
|
+
return { declared: node, parameters: node.parameters };
|
|
48
|
+
}
|
|
49
|
+
if (isVariableDeclaration(node) &&
|
|
50
|
+
node.initializer !== undefined &&
|
|
51
|
+
(isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))) {
|
|
52
|
+
return { declared: node, parameters: node.initializer.parameters };
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Whether one parameter is a `this` parameter, which types `this` and binds no argument. */
|
|
57
|
+
function isThisParameter(parameter) {
|
|
58
|
+
return isIdentifier(parameter.name) && parameter.name.text === "this";
|
|
59
|
+
}
|
|
60
|
+
/** Whether an argument is written in a form that holds no class instance, which saves a type. */
|
|
61
|
+
function holdsNoInstance(argument) {
|
|
62
|
+
return (isArrowFunction(argument) ||
|
|
63
|
+
isFunctionExpression(argument) ||
|
|
64
|
+
isStringLiteral(argument) ||
|
|
65
|
+
isNumericLiteral(argument) ||
|
|
66
|
+
isNoSubstitutionTemplateLiteral(argument) ||
|
|
67
|
+
argument.kind === SyntaxKind.TemplateExpression);
|
|
68
|
+
}
|
|
69
|
+
/** The expression an argument evaluates, through parentheses and a spread. */
|
|
70
|
+
function evaluated(argument) {
|
|
71
|
+
const value = isSpreadElement(argument) ? argument.expression : argument;
|
|
72
|
+
return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Every call of the project's own files with the class `this` stands for there, and
|
|
76
|
+
* every identifier-named parameter of every function, method and function-valued
|
|
77
|
+
* variable the project declares, at the position of the argument it binds.
|
|
78
|
+
*/
|
|
79
|
+
function walkProject(project, held) {
|
|
80
|
+
const sites = [];
|
|
81
|
+
const parameters = [];
|
|
82
|
+
for (const file of project.ownSourceFiles()) {
|
|
83
|
+
const visit = (node, self) => {
|
|
84
|
+
let inner = self;
|
|
85
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
86
|
+
inner = held.declarations.get(nodeKey(file, node));
|
|
87
|
+
}
|
|
88
|
+
else if (resetsThis(node)) {
|
|
89
|
+
inner = undefined;
|
|
90
|
+
}
|
|
91
|
+
if (node.kind === SyntaxKind.CallExpression) {
|
|
92
|
+
sites.push({ call: node, self });
|
|
93
|
+
}
|
|
94
|
+
const fn = functionOf(node);
|
|
95
|
+
const id = fn === undefined ? undefined : held.declarations.get(nodeKey(file, fn.declared));
|
|
96
|
+
if (fn !== undefined && id !== undefined) {
|
|
97
|
+
fn.parameters
|
|
98
|
+
.filter((parameter) => !isThisParameter(parameter))
|
|
99
|
+
.forEach((parameter, index) => {
|
|
100
|
+
if (isIdentifier(parameter.name)) {
|
|
101
|
+
parameters.push({
|
|
102
|
+
fn: id,
|
|
103
|
+
index,
|
|
104
|
+
rest: parameter.dotDotDotToken !== undefined,
|
|
105
|
+
name: parameter.name,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
node.forEachChild((child) => {
|
|
111
|
+
visit(child, inner);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
file.forEachChild((child) => {
|
|
115
|
+
visit(child, undefined);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return { sites, parameters };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The parameters that keep nothing of the value they are given: one typed `unknown` or
|
|
122
|
+
* `any`, and a rest parameter whose elements are, each by its symbol.
|
|
123
|
+
*/
|
|
124
|
+
function erasedParameters(project, parameters) {
|
|
125
|
+
const types = typesAt(project, parameters.map((parameter) => parameter.name));
|
|
126
|
+
const erased = parameters.filter((parameter, index) => {
|
|
127
|
+
const type = types[index];
|
|
128
|
+
if (type === undefined) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (!parameter.rest) {
|
|
132
|
+
return (type.flags & ERASED) !== 0;
|
|
133
|
+
}
|
|
134
|
+
const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
|
|
135
|
+
return element !== undefined && (element.flags & ERASED) !== 0;
|
|
136
|
+
});
|
|
137
|
+
const bySymbol = new Map();
|
|
138
|
+
project
|
|
139
|
+
.symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
|
|
140
|
+
.forEach((symbol, index) => {
|
|
141
|
+
const parameter = erased[index];
|
|
142
|
+
if (symbol !== undefined && parameter !== undefined) {
|
|
143
|
+
bySymbol.set(symbol.id, parameter);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return bySymbol;
|
|
147
|
+
}
|
|
148
|
+
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
149
|
+
function handedOn(project, sites, erased) {
|
|
150
|
+
const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
|
|
151
|
+
const candidates = sites.flatMap((site) => site.call.arguments.flatMap((argument, index) => valuesPassed(argument)
|
|
152
|
+
.filter((value) => spellings.has(value.name.getText()))
|
|
153
|
+
.map((value) => ({ site, argument: index, value }))));
|
|
154
|
+
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
155
|
+
const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
|
|
156
|
+
const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
|
|
157
|
+
return candidates.flatMap((one) => {
|
|
158
|
+
const symbol = one.value.shorthand
|
|
159
|
+
? project.shorthandValueAt(project.handle(one.value.name))
|
|
160
|
+
: symbolOf.get(one);
|
|
161
|
+
const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
|
|
162
|
+
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The destinations of one project's calls. A call is one at every argument where its
|
|
167
|
+
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
168
|
+
* `unknown` or `any` where its callee is declared outside the analysis; and, where its
|
|
169
|
+
* callee is a function of the project, at each parameter the function hands on to a
|
|
170
|
+
* destination, holding back the widest of what those destinations hold back, which is
|
|
171
|
+
* settled by widening until no parameter moves.
|
|
172
|
+
*/
|
|
173
|
+
function destinations(input, chains, walked) {
|
|
174
|
+
const { project, held } = input;
|
|
175
|
+
const checker = project.checker;
|
|
176
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
177
|
+
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
178
|
+
const callees = resolvedTargets(project, walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []));
|
|
179
|
+
const calleeOf = (call) => {
|
|
180
|
+
const name = calleeName(call.expression);
|
|
181
|
+
return name === undefined ? undefined : callees.get(name);
|
|
182
|
+
};
|
|
183
|
+
const erased = erasedParameters(project, walked.parameters);
|
|
184
|
+
const byFunction = new Map();
|
|
185
|
+
for (const parameter of erased.values()) {
|
|
186
|
+
byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
|
|
187
|
+
}
|
|
188
|
+
const outside = (target) => target.declarations.length > 0 &&
|
|
189
|
+
target.declarations.every((handle) => !own.has(handle.path) &&
|
|
190
|
+
!input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)));
|
|
191
|
+
const erasedArguments = new Map();
|
|
192
|
+
const erasedAt = (call, argument) => {
|
|
193
|
+
let indexes = erasedArguments.get(call);
|
|
194
|
+
if (indexes === undefined) {
|
|
195
|
+
const signature = checker.getResolvedSignature(call);
|
|
196
|
+
indexes = new Set(call.arguments.flatMap((_unused, index) => {
|
|
197
|
+
const type = signature === undefined ? undefined : checker.getParameterType(signature, index);
|
|
198
|
+
return type !== undefined && (type.flags & ERASED) !== 0 ? [index] : [];
|
|
199
|
+
}));
|
|
200
|
+
erasedArguments.set(call, indexes);
|
|
201
|
+
}
|
|
202
|
+
return indexes.has(argument);
|
|
203
|
+
};
|
|
204
|
+
const forwards = new Map();
|
|
205
|
+
const keyOf = (parameter) => `${parameter.fn}\u0000${String(parameter.index)}`;
|
|
206
|
+
const at = (call, argument) => {
|
|
207
|
+
const target = calleeOf(call);
|
|
208
|
+
if (target === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const entry = named.find((one) => names(one, target, chains));
|
|
212
|
+
if (entry !== undefined) {
|
|
213
|
+
return { reach: "properties", callee: spelledEntry(entry.entry, held) };
|
|
214
|
+
}
|
|
215
|
+
const declared = chains.declarationsOf(target);
|
|
216
|
+
if (declared.length === 0) {
|
|
217
|
+
return outside(target) && erasedAt(call, argument)
|
|
218
|
+
? { reach: "conversions", callee: call.expression.getText().replace(/\s+/gu, "") }
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
for (const fn of declared) {
|
|
222
|
+
const parameter = (byFunction.get(fn) ?? []).find((one) => one.index === argument || (one.rest && one.index <= argument));
|
|
223
|
+
const reach = parameter === undefined ? undefined : forwards.get(keyOf(parameter));
|
|
224
|
+
if (reach !== undefined) {
|
|
225
|
+
return { reach, callee: held.symbols.find((symbol) => symbol.id === fn)?.name ?? fn };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
};
|
|
230
|
+
const forwarded = handedOn(project, walked.sites, erased);
|
|
231
|
+
for (let moved = true; moved;) {
|
|
232
|
+
moved = false;
|
|
233
|
+
for (const one of forwarded) {
|
|
234
|
+
const key = keyOf(one.parameter);
|
|
235
|
+
const before = forwards.get(key);
|
|
236
|
+
const after = wider(before, at(one.site.call, one.argument)?.reach);
|
|
237
|
+
if (after !== undefined && after !== before) {
|
|
238
|
+
forwards.set(key, after);
|
|
239
|
+
moved = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const mayReceive = (call) => {
|
|
244
|
+
const target = calleeOf(call);
|
|
245
|
+
if (target === undefined) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const declared = chains.declarationsOf(target);
|
|
249
|
+
if (named.some((one) => names(one, target, chains))) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (declared.length === 0) {
|
|
253
|
+
return outside(target);
|
|
254
|
+
}
|
|
255
|
+
return declared.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
256
|
+
};
|
|
257
|
+
return { at, mayReceive };
|
|
258
|
+
}
|
|
259
|
+
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
260
|
+
function flowsOf(project, held, sites, to) {
|
|
261
|
+
const reach = valueReach(project, held);
|
|
262
|
+
const arguments_ = sites.flatMap((site) => to.mayReceive(site.call)
|
|
263
|
+
? site.call.arguments.flatMap((argument, index) => {
|
|
264
|
+
const value = evaluated(argument);
|
|
265
|
+
return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
|
|
266
|
+
})
|
|
267
|
+
: []);
|
|
268
|
+
const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
|
|
269
|
+
const types = typesAt(project, asked.map((one) => one.value));
|
|
270
|
+
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
271
|
+
return arguments_.flatMap((one) => {
|
|
272
|
+
const type = typeOf.get(one.value);
|
|
273
|
+
let classes = new Set();
|
|
274
|
+
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
275
|
+
classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
|
|
276
|
+
}
|
|
277
|
+
else if (type !== undefined) {
|
|
278
|
+
classes = reach.classesOf(type);
|
|
279
|
+
}
|
|
280
|
+
const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
|
|
281
|
+
return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The serialization-contract detector: each property of each class whose values reach
|
|
286
|
+
* a destination, and the two conversion methods where the destination is outside the
|
|
287
|
+
* analysis, recorded at the argument with the callee named.
|
|
288
|
+
*/
|
|
289
|
+
export function serializationContract(input) {
|
|
290
|
+
const { project, held, targetRoot } = input;
|
|
291
|
+
const walked = walkProject(project, held);
|
|
292
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, aliasChains(project, held), walked));
|
|
293
|
+
if (flows.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
|
|
297
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
298
|
+
const found = [];
|
|
299
|
+
for (const flow of flows) {
|
|
300
|
+
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
301
|
+
const detail = `passed to ${flow.to.callee}`;
|
|
302
|
+
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
303
|
+
const symbol = byId.get(member.id);
|
|
304
|
+
const isProperty = (member.flags & SymbolFlags.Property) !== 0;
|
|
305
|
+
const converts = flow.to.reach === "conversions" &&
|
|
306
|
+
(member.flags & SymbolFlags.Method) !== 0 &&
|
|
307
|
+
symbol !== undefined &&
|
|
308
|
+
CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
|
|
309
|
+
if (isProperty || converts) {
|
|
310
|
+
found.push({ id: member.id, detail, site });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return found;
|
|
315
|
+
}
|