@cplieger/deadset-ts 3.4.0 → 3.6.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 +70 -14
- package/dist/src/components.js +66 -107
- package/dist/src/config.js +6 -0
- package/dist/src/dependencies.js +493 -0
- package/dist/src/deprecation.js +83 -0
- package/dist/src/discover.js +1 -1
- package/dist/src/enum-group.js +217 -0
- package/dist/src/file-facts.js +101 -0
- package/dist/src/findings/boundary.js +1 -0
- package/dist/src/findings/completion.js +99 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +65 -1
- package/dist/src/findings/emitters.js +8 -0
- package/dist/src/findings/interfaces.js +95 -2
- package/dist/src/findings/non-code-artifacts.js +97 -1
- package/dist/src/findings/reads-and-writes.js +199 -2
- package/dist/src/findings/unused-declarations.js +182 -1
- package/dist/src/findings/visibility-narrowing.js +239 -2
- package/dist/src/implementations.js +210 -0
- package/dist/src/inventory.js +2 -2
- package/dist/src/kinds.js +40 -0
- package/dist/src/print.js +10 -1
- package/dist/src/resolve.js +91 -10
- package/dist/src/run.js +9 -3
- package/dist/src/schema.js +37 -1
- package/dist/src/scope.js +55 -21
- package/dist/src/serialization-contract.js +109 -19
- package/dist/src/stores.js +32 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +113 -16
- package/src/components.ts +73 -131
- package/src/config.ts +37 -2
- package/src/dependencies.ts +617 -0
- package/src/deprecation.ts +101 -0
- package/src/discover.ts +1 -1
- package/src/enum-group.ts +265 -0
- package/src/file-facts.ts +143 -0
- package/src/findings/boundary.ts +39 -0
- package/src/findings/completion.ts +151 -0
- package/src/findings/dependencies-and-module-machinery.ts +79 -1
- package/src/findings/emitter.ts +21 -1
- package/src/findings/emitters.ts +13 -1
- package/src/findings/interfaces.ts +149 -3
- package/src/findings/non-code-artifacts.ts +124 -2
- package/src/findings/reads-and-writes.ts +310 -3
- package/src/findings/unused-declarations.ts +223 -2
- package/src/findings/visibility-narrowing.ts +324 -3
- package/src/implementations.ts +286 -0
- package/src/inventory.ts +2 -2
- package/src/kinds.ts +41 -0
- package/src/print.ts +17 -1
- package/src/resolve.ts +124 -10
- package/src/run.ts +10 -3
- package/src/schema.ts +47 -2
- package/src/scope.ts +63 -22
- package/src/serialization-contract.ts +138 -20
- package/src/stores.ts +77 -0
- package/src/version.ts +1 -1
package/dist/src/resolve.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { ConfigError, defaultConfig, } from "./config.js";
|
|
2
|
-
import { FIXED_SEVERITY_CODES } from "./kinds.js";
|
|
2
|
+
import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
|
|
3
3
|
import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
|
|
4
4
|
/** Names the command-line flags in a refusal, where a document is named by its path. */
|
|
5
5
|
const FLAGS_LABEL = "the command-line flags";
|
|
6
6
|
/** The length of a severity key naming a whole family: the prefix and two digits. */
|
|
7
|
-
const FAMILY_KEY_LENGTH = 4;
|
|
7
|
+
export const FAMILY_KEY_LENGTH = 4;
|
|
8
8
|
const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
9
9
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
10
10
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
11
|
+
const ANALYZER_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/u;
|
|
12
|
+
const PROVIDER_SOURCE = /^(go|npm):[^ \t\r\n]+$/u;
|
|
13
|
+
const PROVIDER_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/u;
|
|
14
|
+
const PROVIDER_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
15
|
+
/** The members only an acquirable provider entry carries. */
|
|
16
|
+
const ACQUIRABLE_MEMBERS = ["source", "version", "digest"];
|
|
11
17
|
/** The members only a platform build configuration carries. */
|
|
12
18
|
const PLATFORM_MEMBERS = ["os", "arch", "tags"];
|
|
13
19
|
/** The member only a project build configuration carries. */
|
|
@@ -223,8 +229,8 @@ function readDelimiters(analysis, label) {
|
|
|
223
229
|
};
|
|
224
230
|
}
|
|
225
231
|
/**
|
|
226
|
-
* A severity key that names no setting
|
|
227
|
-
* the kind or kinds it names.
|
|
232
|
+
* A severity key that names no setting: it names no live kind, or the Contract
|
|
233
|
+
* fixes the severity of the kind or kinds it names.
|
|
228
234
|
*/
|
|
229
235
|
function unimplementedSeverityKey(label, path, reason) {
|
|
230
236
|
return new ConfigError("unimplemented-key", path, `${label}: key ${JSON.stringify(path)} is not implemented: ${reason}`);
|
|
@@ -245,16 +251,19 @@ function spellCodes(codes) {
|
|
|
245
251
|
}
|
|
246
252
|
return `${codes.slice(0, -1).join(", ")} and ${String(codes[codes.length - 1])}`;
|
|
247
253
|
}
|
|
254
|
+
/** Whether one severity key names a live kind: the code itself, or a family holding one. */
|
|
255
|
+
function namesLiveKind(code) {
|
|
256
|
+
return LIVE_CODES.some((candidate) => code === candidate || (code.length === FAMILY_KEY_LENGTH && candidate.startsWith(code)));
|
|
257
|
+
}
|
|
248
258
|
/**
|
|
249
259
|
* The severity object, one code at a time.
|
|
250
260
|
*
|
|
251
|
-
* A key is one issue-kind code or one two-digit family prefix
|
|
252
|
-
* kind whose severity the Contract fixes
|
|
253
|
-
* one, is an unimplemented key rather than a setting.
|
|
261
|
+
* A key is one issue-kind code or one two-digit family prefix. A key naming no
|
|
262
|
+
* live kind, and a key naming a kind whose severity the Contract fixes or a family
|
|
263
|
+
* prefix whose range holds one, is an unimplemented key rather than a setting.
|
|
254
264
|
*
|
|
255
|
-
* Whether a key names a kind this analyzer reports is not decided here
|
|
256
|
-
*
|
|
257
|
-
* and asking it would refuse the whole object.
|
|
265
|
+
* Whether a key names a kind this analyzer reports is not decided here: a live
|
|
266
|
+
* kind of another language is a setting the analyzer of that language reads.
|
|
258
267
|
*/
|
|
259
268
|
function readSeverity(document, label) {
|
|
260
269
|
const object = section(document, SEVERITY_SECTION, SEVERITY_SECTION, label);
|
|
@@ -267,6 +276,11 @@ function readSeverity(document, label) {
|
|
|
267
276
|
if (!SEVERITY_KEY.test(code)) {
|
|
268
277
|
throw unimplementedSeverityKey(label, path, "a severity key is one issue-kind code or one two-digit family prefix");
|
|
269
278
|
}
|
|
279
|
+
if (!namesLiveKind(code)) {
|
|
280
|
+
throw unimplementedSeverityKey(label, path, code.length === FAMILY_KEY_LENGTH
|
|
281
|
+
? "the family this prefix names holds no live issue kind"
|
|
282
|
+
: "the code names no live issue kind");
|
|
283
|
+
}
|
|
270
284
|
const fixed = fixedByContract(code);
|
|
271
285
|
if (fixed.length > 0) {
|
|
272
286
|
throw unimplementedSeverityKey(label, path, `the Contract fixes the severity of ${spellCodes(fixed)}, which this key names`);
|
|
@@ -384,6 +398,68 @@ function readLifecycleContracts(ts, label) {
|
|
|
384
398
|
}
|
|
385
399
|
return value.map((entry, index) => readLifecycleContract(entry, `${path}[${String(index)}]`, label));
|
|
386
400
|
}
|
|
401
|
+
/** One string member a provider entry requires, refused where it fails `pattern`. */
|
|
402
|
+
function readMatching(entry, name, at, label, pattern) {
|
|
403
|
+
const path = `${at}.${name}`;
|
|
404
|
+
const value = requireMember(readString(entry, name, path, label), path, label);
|
|
405
|
+
if (!pattern.test(value)) {
|
|
406
|
+
throw malformed(label, path, `${JSON.stringify(value)} does not match ${pattern.source}`);
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* One provider entry, read as the one shape its members name: a member only an
|
|
412
|
+
* acquirable analyzer carries makes it acquirable, and then it carries all three.
|
|
413
|
+
*/
|
|
414
|
+
function readProvider(entry, at, label) {
|
|
415
|
+
if (!isRecord(entry)) {
|
|
416
|
+
throw malformed(label, at, "is not an object");
|
|
417
|
+
}
|
|
418
|
+
const name = readMatching(entry, "name", at, label, ANALYZER_NAME);
|
|
419
|
+
const languages = readStrings(entry, "languages", `${at}.languages`, label, 1, LANGUAGES);
|
|
420
|
+
if (languages === undefined) {
|
|
421
|
+
throw malformed(label, `${at}.languages`, "is required and names nothing");
|
|
422
|
+
}
|
|
423
|
+
const commandPath = `${at}.command`;
|
|
424
|
+
const command = requireMember(readString(entry, "command", commandPath, label), commandPath, label);
|
|
425
|
+
if (!ACQUIRABLE_MEMBERS.some((member) => Object.hasOwn(entry, member))) {
|
|
426
|
+
return { shape: "installed", name, languages, command };
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
shape: "acquirable",
|
|
430
|
+
name,
|
|
431
|
+
languages,
|
|
432
|
+
command,
|
|
433
|
+
source: readMatching(entry, "source", at, label, PROVIDER_SOURCE),
|
|
434
|
+
version: readMatching(entry, "version", at, label, PROVIDER_VERSION),
|
|
435
|
+
digest: readMatching(entry, "digest", at, label, PROVIDER_DIGEST),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The provider list. Every file a run writes for an analyzer is keyed by the
|
|
440
|
+
* entry's name, so a list naming one name twice is refused at the later entry.
|
|
441
|
+
*/
|
|
442
|
+
function readProviders(providers, label) {
|
|
443
|
+
const path = "providers.analyzers";
|
|
444
|
+
const value = member(providers, "analyzers");
|
|
445
|
+
if (value === undefined) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (!Array.isArray(value)) {
|
|
449
|
+
throw malformed(label, path, "is not an array");
|
|
450
|
+
}
|
|
451
|
+
const read = value.map((entry, index) => readProvider(entry, `${path}[${String(index)}]`, label));
|
|
452
|
+
const first = new Map();
|
|
453
|
+
read.forEach((entry, index) => {
|
|
454
|
+
const earlier = first.get(entry.name);
|
|
455
|
+
if (earlier !== undefined) {
|
|
456
|
+
throw malformed(label, `${path}[${String(index)}].name`, `${JSON.stringify(entry.name)} is already the name of ${path}[${String(earlier)}], ` +
|
|
457
|
+
"and every entry's name keys the files a run writes for it");
|
|
458
|
+
}
|
|
459
|
+
first.set(entry.name, index);
|
|
460
|
+
});
|
|
461
|
+
return read;
|
|
462
|
+
}
|
|
387
463
|
/**
|
|
388
464
|
* Reads one already-parsed configuration document as the settings it supplies,
|
|
389
465
|
* refusing every constraint the closed key list declares that a parse cannot
|
|
@@ -400,6 +476,7 @@ function readDoc(value, label) {
|
|
|
400
476
|
const consumers = section(value, "consumers", "consumers", label);
|
|
401
477
|
const roots = section(value, "roots", "roots", label);
|
|
402
478
|
const reporters = section(value, "reporters", "reporters", label);
|
|
479
|
+
const providers = section(value, "providers", "providers", label);
|
|
403
480
|
const ts = section(value, "ts", "ts", label);
|
|
404
481
|
section(value, "go", "go", label);
|
|
405
482
|
section(value, "provenance", "provenance", label);
|
|
@@ -427,6 +504,7 @@ function readDoc(value, label) {
|
|
|
427
504
|
cascade: readEnum(reporters, "cascade", "reporters.cascade", label, CASCADES),
|
|
428
505
|
maxFindings: readCount(reporters, "max_findings", "reporters.max_findings", label),
|
|
429
506
|
failOn: readEnum(reporters, "fail_on", "reporters.fail_on", label, SEVERITIES),
|
|
507
|
+
providers: readProviders(providers, label),
|
|
430
508
|
testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
|
|
431
509
|
entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
|
|
432
510
|
injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
|
|
@@ -630,6 +708,9 @@ function settleAll(inputs) {
|
|
|
630
708
|
maxFindings: at("reporters.max_findings", defaults.reporters.maxFindings, (doc) => doc.maxFindings),
|
|
631
709
|
failOn: at("reporters.fail_on", defaults.reporters.failOn, (doc) => doc.failOn),
|
|
632
710
|
},
|
|
711
|
+
providers: {
|
|
712
|
+
analyzers: at("providers.analyzers", defaults.providers.analyzers, (doc) => doc.providers),
|
|
713
|
+
},
|
|
633
714
|
ts: {
|
|
634
715
|
testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
|
|
635
716
|
entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
|
package/dist/src/run.js
CHANGED
|
@@ -47,8 +47,14 @@ export const SETTING_OPTIONS = new Map([
|
|
|
47
47
|
]);
|
|
48
48
|
/** The options that take a value and are not settings. */
|
|
49
49
|
const PLAIN_OPTIONS = new Set(["target", "config", "central", "scope"]);
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The setting options whose value is an integer rather than a string. A value
|
|
52
|
+
* written other than as digits is passed on as the string it is, so resolution
|
|
53
|
+
* refuses it by its type rather than reading `1e2` as 100.
|
|
54
|
+
*/
|
|
55
|
+
const INTEGER_OPTIONS = new Set(["max-findings"]);
|
|
56
|
+
/** An integer as an option writes it: an optional minus sign and digits alone. */
|
|
57
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
52
58
|
/**
|
|
53
59
|
* The first option whose name carries a source-edit token, because every verb
|
|
54
60
|
* reports and never edits. The match is on the name alone, so an option whose
|
|
@@ -116,7 +122,7 @@ function inputsOf(host, options) {
|
|
|
116
122
|
if (value === undefined) {
|
|
117
123
|
continue;
|
|
118
124
|
}
|
|
119
|
-
flat[path] =
|
|
125
|
+
flat[path] = INTEGER_OPTIONS.has(name) && INTEGER_TEXT.test(value) ? Number(value) : value;
|
|
120
126
|
flagLabels.set(path, `--${name}`);
|
|
121
127
|
}
|
|
122
128
|
const flags = Object.keys(flat).length === 0 ? undefined : JSON.stringify(flat);
|
package/dist/src/schema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigError } from "./config.js";
|
|
2
2
|
const leaf = { kind: "leaf" };
|
|
3
|
+
const integer = { kind: "leaf", integer: true };
|
|
3
4
|
/** A list of declarations, each entry in one of the three shapes the `ts` section states. */
|
|
4
5
|
const declarations = {
|
|
5
6
|
kind: "list",
|
|
@@ -44,10 +45,26 @@ export const SCHEMA_ROOT = {
|
|
|
44
45
|
formats: leaf,
|
|
45
46
|
sort: leaf,
|
|
46
47
|
cascade: leaf,
|
|
47
|
-
max_findings:
|
|
48
|
+
max_findings: integer,
|
|
48
49
|
fail_on: leaf,
|
|
49
50
|
},
|
|
50
51
|
},
|
|
52
|
+
providers: {
|
|
53
|
+
kind: "section",
|
|
54
|
+
members: {
|
|
55
|
+
analyzers: {
|
|
56
|
+
kind: "list",
|
|
57
|
+
members: {
|
|
58
|
+
name: leaf,
|
|
59
|
+
languages: leaf,
|
|
60
|
+
command: leaf,
|
|
61
|
+
source: leaf,
|
|
62
|
+
version: leaf,
|
|
63
|
+
digest: leaf,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
51
68
|
go: { kind: "section", members: {} },
|
|
52
69
|
ts: {
|
|
53
70
|
kind: "section",
|
|
@@ -244,6 +261,21 @@ function skipScalar(cursor) {
|
|
|
244
261
|
cursor.at += 1;
|
|
245
262
|
}
|
|
246
263
|
}
|
|
264
|
+
/** A number the document writes as an optional minus sign and digits alone. */
|
|
265
|
+
const INTEGER_TEXT = /^-?[0-9]+$/u;
|
|
266
|
+
/** The start of a JSON number: a minus sign or a digit. */
|
|
267
|
+
const NUMBER_START = /^[-0-9]/u;
|
|
268
|
+
/**
|
|
269
|
+
* Refuses a number written with a fraction or an exponent where the key list
|
|
270
|
+
* declares an integer. A parse reads `100.0` and `1e2` as the integer 100, so only
|
|
271
|
+
* the text can tell them apart. A scalar that is no number is the decoder's to
|
|
272
|
+
* refuse by its type.
|
|
273
|
+
*/
|
|
274
|
+
function checkInteger(text, at, label) {
|
|
275
|
+
if (NUMBER_START.test(text) && !INTEGER_TEXT.test(text)) {
|
|
276
|
+
throw malformed(label, at, `${text} is written with a fraction or an exponent, and an integer is digits alone`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
247
279
|
function walkValue(cursor, at, node, label) {
|
|
248
280
|
skipSpace(cursor);
|
|
249
281
|
const ch = cursor.text.charCodeAt(cursor.at);
|
|
@@ -261,7 +293,11 @@ function walkValue(cursor, at, node, label) {
|
|
|
261
293
|
readName(cursor, at, label);
|
|
262
294
|
return;
|
|
263
295
|
}
|
|
296
|
+
const start = cursor.at;
|
|
264
297
|
skipScalar(cursor);
|
|
298
|
+
if (node.integer === true) {
|
|
299
|
+
checkInteger(cursor.text.slice(start, cursor.at), at, label);
|
|
300
|
+
}
|
|
265
301
|
}
|
|
266
302
|
function walkObject(cursor, at, node, label) {
|
|
267
303
|
const seen = new Set();
|
package/dist/src/scope.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ConfigError } from "./config.js";
|
|
1
2
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
3
|
+
import { checkDocument } from "./schema.js";
|
|
2
4
|
/** The roles a scope document declares for a module. */
|
|
3
5
|
export const ROLE_TARGET = "target";
|
|
4
6
|
export const ROLE_CONSUMER = "consumer";
|
|
@@ -17,18 +19,37 @@ export class ScopeError extends Error {
|
|
|
17
19
|
this.name = "ScopeError";
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const MODULE = {
|
|
23
|
+
kind: "section",
|
|
24
|
+
members: { id: { kind: "leaf" }, role: { kind: "leaf" }, path: { kind: "leaf" } },
|
|
25
|
+
};
|
|
26
|
+
/** The closed key list of the scope document, compared as bytes. */
|
|
27
|
+
const SCOPE_ROOT = {
|
|
28
|
+
kind: "section",
|
|
29
|
+
members: {
|
|
30
|
+
target: MODULE,
|
|
31
|
+
workspace: { kind: "leaf" },
|
|
32
|
+
consumers: { kind: "list", members: MODULE.members ?? {} },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
22
35
|
function isRecord(value) {
|
|
23
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* One string member, or undefined where the document omits it. A member the
|
|
40
|
+
* document writes is a string holding at least one character, so `null` and `""`
|
|
41
|
+
* are refused rather than read as absent.
|
|
42
|
+
*/
|
|
25
43
|
function stringMember(parent, name, at) {
|
|
26
44
|
if (!Object.hasOwn(parent, name)) {
|
|
27
|
-
return
|
|
45
|
+
return undefined;
|
|
28
46
|
}
|
|
29
47
|
const value = parent[name];
|
|
30
48
|
if (typeof value !== "string") {
|
|
31
|
-
throw new ScopeError(`scope: ${at}.${name} is not a string`);
|
|
49
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} is not a string`);
|
|
50
|
+
}
|
|
51
|
+
if (value === "") {
|
|
52
|
+
throw new ScopeError(`scope: ${at === "" ? name : `${at}.${name}`} names nothing`);
|
|
32
53
|
}
|
|
33
54
|
return value;
|
|
34
55
|
}
|
|
@@ -36,30 +57,46 @@ function readModule(value, at, role, documentDir) {
|
|
|
36
57
|
if (!isRecord(value)) {
|
|
37
58
|
throw new ScopeError(`scope: ${at} is not an object`);
|
|
38
59
|
}
|
|
39
|
-
for (const name of Object.keys(value)) {
|
|
40
|
-
if (!MODULE_KEYS.has(name)) {
|
|
41
|
-
throw new ScopeError(`scope: ${at}.${name} is not a declared key`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
60
|
const declared = stringMember(value, "role", at);
|
|
45
|
-
if (declared !==
|
|
61
|
+
if (declared !== undefined && declared !== role) {
|
|
46
62
|
throw new ScopeError(`scope: ${at}.role is ${JSON.stringify(declared)}, want ${JSON.stringify(role)}`);
|
|
47
63
|
}
|
|
48
64
|
const path = stringMember(value, "path", at);
|
|
49
|
-
if (path ===
|
|
65
|
+
if (path === undefined) {
|
|
50
66
|
throw new ScopeError(`scope: ${at}.path names nothing`);
|
|
51
67
|
}
|
|
52
68
|
return {
|
|
53
|
-
id: stringMember(value, "id", at),
|
|
69
|
+
id: stringMember(value, "id", at) ?? "",
|
|
54
70
|
path: isAbsolutePath(path) ? path : joinPath(documentDir, path),
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
74
|
+
* Walks the document's text against the closed key list, refusing a key the list
|
|
75
|
+
* does not declare, compared as bytes, and a key one object writes twice, which a
|
|
76
|
+
* parse cannot report because it keeps the last value.
|
|
77
|
+
*/
|
|
78
|
+
function checkKeys(text, file) {
|
|
79
|
+
try {
|
|
80
|
+
checkDocument(text, SCOPE_ROOT, `scope: ${file}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof ConfigError) {
|
|
84
|
+
// The configuration's refusal names the nearest configuration key, which a
|
|
85
|
+
// scope document does not have.
|
|
86
|
+
throw new ScopeError(error.kind === "unimplemented-key"
|
|
87
|
+
? `scope: ${file}: ${error.key} is not a declared key`
|
|
88
|
+
: error.message);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decodes the scope document at `file`. Decoding is strict: an undeclared key, a
|
|
95
|
+
* key written twice in one object, and a value of a type the document does not
|
|
96
|
+
* admit, `null` included, are errors, and a relative path inside the document
|
|
97
|
+
* resolves against the document's own directory.
|
|
61
98
|
*
|
|
62
|
-
* The `workspace` key the format declares is
|
|
99
|
+
* The `workspace` key the format declares is checked and carries nothing here: it
|
|
63
100
|
* names the file a Go consumer resolves the target through, and a TypeScript
|
|
64
101
|
* project resolves one through its own compiler configuration.
|
|
65
102
|
*/
|
|
@@ -85,11 +122,8 @@ export function readScope(host, file) {
|
|
|
85
122
|
if (!isRecord(value)) {
|
|
86
123
|
throw new ScopeError(`scope: ${file} is not one JSON object`);
|
|
87
124
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new ScopeError(`scope: ${file}: ${name} is not a declared key`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
125
|
+
checkKeys(text, file);
|
|
126
|
+
stringMember(value, "workspace", "");
|
|
93
127
|
const documentDir = dirnamePath(absolute);
|
|
94
128
|
if (!Object.hasOwn(value, "target")) {
|
|
95
129
|
throw new ScopeError(`scope: ${file} names no target`);
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
|
|
4
4
|
* `toString` where a value is passed to an `unknown` or `any` parameter of a function
|
|
5
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.
|
|
6
|
+
* to a destination is one, holding back what that destination does, to a fixpoint. A
|
|
7
|
+
* function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
|
|
8
|
+
* given the arguments those methods pass it.
|
|
7
9
|
*/
|
|
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 { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
11
|
+
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
10
12
|
import { aliasChains } from "./alias-chain.js";
|
|
11
13
|
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
12
14
|
import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
@@ -71,6 +73,55 @@ function evaluated(argument) {
|
|
|
71
73
|
const value = isSpreadElement(argument) ? argument.expression : argument;
|
|
72
74
|
return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
|
|
73
75
|
}
|
|
76
|
+
/** The methods of `Function.prototype` that invoke a function with arguments of their own. */
|
|
77
|
+
const FUNCTION_METHODS = new Set(["call", "apply", "bind"]);
|
|
78
|
+
/** The interfaces of the standard library that declare those methods for a function value. */
|
|
79
|
+
const FUNCTION_INTERFACES = new Set([
|
|
80
|
+
"Function",
|
|
81
|
+
"CallableFunction",
|
|
82
|
+
"NewableFunction",
|
|
83
|
+
]);
|
|
84
|
+
function unparenthesized(expression) {
|
|
85
|
+
return isParenthesizedExpression(expression)
|
|
86
|
+
? unparenthesized(expression.expression)
|
|
87
|
+
: expression;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The invocation a call writes through a method of `Function.prototype`, read from
|
|
91
|
+
* the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
|
|
92
|
+
* binds `a`, and `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
|
|
93
|
+
* that is no array literal is the value of the first parameter, whose elements it
|
|
94
|
+
* carries. Whether the method is the prototype's is resolution's to say.
|
|
95
|
+
*/
|
|
96
|
+
function throughMethod(call) {
|
|
97
|
+
const expression = unparenthesized(call.expression);
|
|
98
|
+
if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
|
|
99
|
+
const [, ...rest] = call.arguments;
|
|
100
|
+
let passed = rest;
|
|
101
|
+
if (expression.name.text === "apply") {
|
|
102
|
+
const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
|
|
103
|
+
passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
method: expression.name,
|
|
107
|
+
invocation: { callee: expression.expression, arguments: passed, indirect: true },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (isCallExpression(expression)) {
|
|
111
|
+
const bound = unparenthesized(expression.expression);
|
|
112
|
+
if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
|
|
113
|
+
return {
|
|
114
|
+
method: bound.name,
|
|
115
|
+
invocation: {
|
|
116
|
+
callee: bound.expression,
|
|
117
|
+
arguments: [...expression.arguments.slice(1), ...call.arguments],
|
|
118
|
+
indirect: true,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
74
125
|
/**
|
|
75
126
|
* Every call of the project's own files with the class `this` stands for there, and
|
|
76
127
|
* every identifier-named parameter of every function, method and function-valued
|
|
@@ -146,9 +197,9 @@ function erasedParameters(project, parameters) {
|
|
|
146
197
|
return bySymbol;
|
|
147
198
|
}
|
|
148
199
|
/** Each place a call's argument passes one of the erased parameters it can see. */
|
|
149
|
-
function handedOn(project, sites, erased) {
|
|
200
|
+
function handedOn(project, sites, erased, invocationOf) {
|
|
150
201
|
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)
|
|
202
|
+
const candidates = sites.flatMap((site) => invocationOf(site.call).arguments.flatMap((argument, index) => valuesPassed(argument)
|
|
152
203
|
.filter((value) => spellings.has(value.name.getText()))
|
|
153
204
|
.map((value) => ({ site, argument: index, value }))));
|
|
154
205
|
const plain = candidates.filter((one) => !one.value.shorthand);
|
|
@@ -175,11 +226,20 @@ function destinations(input, chains, walked) {
|
|
|
175
226
|
const checker = project.checker;
|
|
176
227
|
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
177
228
|
const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(namesADeclaration);
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
229
|
+
const forms = new Map();
|
|
230
|
+
for (const site of walked.sites) {
|
|
231
|
+
const form = throughMethod(site.call);
|
|
232
|
+
if (form !== undefined) {
|
|
233
|
+
forms.set(site.call, form);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const callees = resolvedTargets(project, [
|
|
237
|
+
...walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
|
|
238
|
+
...[...forms.values()].flatMap((form) => {
|
|
239
|
+
const name = calleeName(form.invocation.callee);
|
|
240
|
+
return name === undefined ? [form.method] : [form.method, name];
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
183
243
|
const erased = erasedParameters(project, walked.parameters);
|
|
184
244
|
const byFunction = new Map();
|
|
185
245
|
for (const parameter of erased.values()) {
|
|
@@ -188,15 +248,42 @@ function destinations(input, chains, walked) {
|
|
|
188
248
|
const outside = (target) => target.declarations.length > 0 &&
|
|
189
249
|
target.declarations.every((handle) => !own.has(handle.path) &&
|
|
190
250
|
!input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)));
|
|
251
|
+
const invocation = (call) => {
|
|
252
|
+
const form = forms.get(call);
|
|
253
|
+
const method = form === undefined ? undefined : callees.get(form.method);
|
|
254
|
+
// Only the standard library's function methods pass their arguments on: the
|
|
255
|
+
// program's own `call` and `Reflect.apply` invoke nothing here.
|
|
256
|
+
if (form !== undefined &&
|
|
257
|
+
method !== undefined &&
|
|
258
|
+
outside(method) &&
|
|
259
|
+
FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")) {
|
|
260
|
+
return form.invocation;
|
|
261
|
+
}
|
|
262
|
+
return { callee: call.expression, arguments: call.arguments, indirect: false };
|
|
263
|
+
};
|
|
264
|
+
const calleeOf = (call) => {
|
|
265
|
+
const name = calleeName(invocation(call).callee);
|
|
266
|
+
return name === undefined ? undefined : callees.get(name);
|
|
267
|
+
};
|
|
191
268
|
const erasedArguments = new Map();
|
|
192
269
|
const erasedAt = (call, argument) => {
|
|
193
270
|
let indexes = erasedArguments.get(call);
|
|
194
271
|
if (indexes === undefined) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
272
|
+
const invoked = invocation(call);
|
|
273
|
+
// Through a method of the prototype the resolved signature is the method's, so
|
|
274
|
+
// the parameters are read off the callee's own signatures, any one erasing.
|
|
275
|
+
const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
|
|
276
|
+
const signatures = invoked.indirect
|
|
277
|
+
? type === undefined
|
|
278
|
+
? []
|
|
279
|
+
: checker.getSignaturesOfType(type, SignatureKind.Call)
|
|
280
|
+
: [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
|
|
281
|
+
indexes = new Set(invoked.arguments.flatMap((_unused, index) => signatures.some((signature) => {
|
|
282
|
+
const parameter = checker.getParameterType(signature, index);
|
|
283
|
+
return parameter !== undefined && (parameter.flags & ERASED) !== 0;
|
|
284
|
+
})
|
|
285
|
+
? [index]
|
|
286
|
+
: []));
|
|
200
287
|
erasedArguments.set(call, indexes);
|
|
201
288
|
}
|
|
202
289
|
return indexes.has(argument);
|
|
@@ -215,7 +302,10 @@ function destinations(input, chains, walked) {
|
|
|
215
302
|
const declared = chains.declarationsOf(target);
|
|
216
303
|
if (declared.length === 0) {
|
|
217
304
|
return outside(target) && erasedAt(call, argument)
|
|
218
|
-
? {
|
|
305
|
+
? {
|
|
306
|
+
reach: "conversions",
|
|
307
|
+
callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
|
|
308
|
+
}
|
|
219
309
|
: undefined;
|
|
220
310
|
}
|
|
221
311
|
for (const fn of declared) {
|
|
@@ -227,7 +317,7 @@ function destinations(input, chains, walked) {
|
|
|
227
317
|
}
|
|
228
318
|
return undefined;
|
|
229
319
|
};
|
|
230
|
-
const forwarded = handedOn(project, walked.sites, erased);
|
|
320
|
+
const forwarded = handedOn(project, walked.sites, erased, invocation);
|
|
231
321
|
for (let moved = true; moved;) {
|
|
232
322
|
moved = false;
|
|
233
323
|
for (const one of forwarded) {
|
|
@@ -254,13 +344,13 @@ function destinations(input, chains, walked) {
|
|
|
254
344
|
}
|
|
255
345
|
return declared.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
256
346
|
};
|
|
257
|
-
return { at, mayReceive };
|
|
347
|
+
return { invocation, at, mayReceive };
|
|
258
348
|
}
|
|
259
349
|
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
260
350
|
function flowsOf(project, held, sites, to) {
|
|
261
351
|
const reach = valueReach(project, held);
|
|
262
352
|
const arguments_ = sites.flatMap((site) => to.mayReceive(site.call)
|
|
263
|
-
? site.call.arguments.flatMap((argument, index) => {
|
|
353
|
+
? to.invocation(site.call).arguments.flatMap((argument, index) => {
|
|
264
354
|
const value = evaluated(argument);
|
|
265
355
|
return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
|
|
266
356
|
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run knows about the stores into its declarations and the reads of them, which
|
|
3
|
+
* a sweep does not keep: a sweep asks whether anything names a declaration, and a store
|
|
4
|
+
* names it as surely as a read does.
|
|
5
|
+
*/
|
|
6
|
+
import { isGetAccessorDeclaration, isSetAccessorDeclaration, } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { nodeKey } from "./inventory.js";
|
|
8
|
+
/** The class members one project's own files declare by a getter or a setter. */
|
|
9
|
+
export function accessorsOf(project, held) {
|
|
10
|
+
const found = [];
|
|
11
|
+
for (const file of project.ownSourceFiles()) {
|
|
12
|
+
const visit = (node) => {
|
|
13
|
+
if (isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) {
|
|
14
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
15
|
+
if (id !== undefined) {
|
|
16
|
+
found.push(id);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
node.forEachChild(visit);
|
|
20
|
+
};
|
|
21
|
+
file.forEachChild(visit);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
/** The run's stores, from what each configuration contributed, the records and the mode. */
|
|
26
|
+
export function storesOf(per, exempt, mode) {
|
|
27
|
+
return {
|
|
28
|
+
references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
|
|
29
|
+
accessors: new Set(per.flatMap((one) => one.accessors)),
|
|
30
|
+
exempt: new Set(exempt.map((record) => record.id)),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED