@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.
- package/README.md +4 -4
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +217 -48
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/components.js +4 -2
- package/dist/src/configured-declarations.js +56 -7
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/dependencies.js +34 -17
- package/dist/src/deprecation.js +35 -24
- package/dist/src/destructuring.js +37 -29
- package/dist/src/discover.js +105 -28
- package/dist/src/edges.js +4 -1
- package/dist/src/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/completion.js +37 -8
- package/dist/src/findings/emitters.js +20 -6
- package/dist/src/findings/interfaces.js +7 -1
- package/dist/src/findings/intra-function.js +91 -2
- package/dist/src/findings/self-check.js +30 -7
- package/dist/src/findings/unused-declarations.js +15 -3
- package/dist/src/findings-pass.js +18 -11
- package/dist/src/framework-lifecycle.js +8 -6
- package/dist/src/implementations.js +45 -23
- package/dist/src/interface-satisfaction.js +30 -27
- package/dist/src/intra-function-parts.js +660 -0
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- package/dist/src/kinds.js +337 -35
- package/dist/src/partial.js +23 -0
- package/dist/src/query.js +207 -0
- package/dist/src/references.js +92 -20
- package/dist/src/reflective-lookup.js +46 -4
- package/dist/src/report.js +14 -3
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +22 -16
- package/dist/src/reporters/template.js +112 -12
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +53 -32
- package/dist/src/session.js +79 -39
- package/dist/src/signatures.js +32 -0
- package/dist/src/suppress-file.js +29 -7
- package/dist/src/suppress-inline.js +8 -1
- package/dist/src/suppress.js +27 -0
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +44 -25
- package/dist/src/verbs/analyze.js +39 -14
- package/dist/src/verbs/describe.js +12 -8
- package/dist/src/verbs/explain.js +22 -9
- package/dist/src/verbs/print-projects.js +40 -7
- package/dist/src/verbs/print-retained.js +6 -2
- package/dist/src/verbs/print-roots.js +5 -1
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +385 -104
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/components.ts +4 -2
- package/src/configured-declarations.ts +80 -8
- package/src/conformance-record.ts +2 -2
- package/src/dependencies.ts +42 -20
- package/src/deprecation.ts +53 -27
- package/src/destructuring.ts +50 -39
- package/src/discover.ts +177 -33
- package/src/edges.ts +4 -1
- package/src/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +46 -10
- package/src/findings/emitter.ts +3 -0
- package/src/findings/emitters.ts +27 -6
- package/src/findings/interfaces.ts +7 -1
- package/src/findings/intra-function.ts +106 -3
- package/src/findings/self-check.ts +48 -7
- package/src/findings/unused-declarations.ts +18 -4
- package/src/findings-pass.ts +26 -11
- package/src/framework-lifecycle.ts +10 -6
- package/src/host.ts +5 -3
- package/src/implementations.ts +59 -23
- package/src/interface-satisfaction.ts +30 -27
- package/src/intra-function-parts.ts +889 -0
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/kinds.ts +355 -41
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +56 -6
- package/src/report.ts +32 -4
- package/src/reporters/annotations.ts +23 -21
- package/src/reporters/json.ts +3 -2
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/sarif.ts +27 -18
- package/src/reporters/template.ts +119 -14
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +59 -40
- package/src/session.ts +146 -58
- package/src/signatures.ts +55 -0
- package/src/suppress-file.ts +40 -8
- package/src/suppress-inline.ts +8 -1
- package/src/suppress.ts +31 -0
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +66 -28
- package/src/verbs/analyze.ts +51 -14
- package/src/verbs/describe.ts +13 -9
- package/src/verbs/explain.ts +20 -9
- package/src/verbs/print-projects.ts +53 -7
- package/src/verbs/print-retained.ts +6 -2
- package/src/verbs/print-roots.ts +5 -1
- package/src/version.ts +1 -1
- package/dist/src/reporters/rules.js +0 -178
- package/src/reporters/rules.ts +0 -195
|
@@ -1,2 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { KINDS } from "../kinds.js";
|
|
2
|
+
import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
|
|
3
|
+
/** A list of rules as a sentence names them. */
|
|
4
|
+
function spelled(rules) {
|
|
5
|
+
if (rules.length < 2) {
|
|
6
|
+
return rules.join("");
|
|
7
|
+
}
|
|
8
|
+
return `${rules.slice(0, -1).join(", ")} and ${rules.at(-1) ?? ""}`;
|
|
9
|
+
}
|
|
10
|
+
/** The clause a message ends with: the other rules that report the kind, or that none is known. */
|
|
11
|
+
function overlapClause(overlap) {
|
|
12
|
+
return overlap.length === 1 && overlap[0] === "none known"
|
|
13
|
+
? "no other rule is known to report it"
|
|
14
|
+
: `also reported by ${spelled(overlap)}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether one part is reported. A parameter needs its function's signature free to
|
|
18
|
+
* change; a result needs that, every call in the loaded program to discard it, and every
|
|
19
|
+
* call site to be in the loaded program, so a function with a root or one a module
|
|
20
|
+
* exports to callers the run cannot see has no result to report. Every other part is
|
|
21
|
+
* reported wherever no project found it in use.
|
|
22
|
+
*/
|
|
23
|
+
function reported(input, part, callersUnknown) {
|
|
24
|
+
if (part.used) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const { free, discardedEverywhere } = input.intraFunction;
|
|
28
|
+
switch (part.code) {
|
|
29
|
+
case UNUSED_PARAMETER:
|
|
30
|
+
return free.has(part.declaration);
|
|
31
|
+
case UNUSED_RESULT:
|
|
32
|
+
return (free.has(part.declaration) &&
|
|
33
|
+
discardedEverywhere.has(part.declaration) &&
|
|
34
|
+
!callersUnknown.has(part.declaration));
|
|
35
|
+
default:
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
|
|
41
|
+
* the declaration its reference names, at the part's own position, whatever the sweep
|
|
42
|
+
* decided about that declaration. Each message names the other rules that report the
|
|
43
|
+
* kind, as the vocabulary lists them, and so does the finding's details.
|
|
44
|
+
*/
|
|
45
|
+
export const intraFunction = (input) => {
|
|
46
|
+
const union = input.swept.matrix.union;
|
|
47
|
+
const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
48
|
+
const unknown = new Set(union.rooted.flatMap((root) => {
|
|
49
|
+
const symbol = union.symbols[root.at];
|
|
50
|
+
return symbol === undefined ? [] : [symbol.id];
|
|
51
|
+
}));
|
|
52
|
+
const { declared, loaded } = input.boundary.consumers;
|
|
53
|
+
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
54
|
+
const published = (id) => {
|
|
55
|
+
const symbol = byId.get(id);
|
|
56
|
+
return (symbol !== undefined &&
|
|
57
|
+
symbol.kind !== "file" &&
|
|
58
|
+
(symbol.exported || published(symbol.parent)));
|
|
59
|
+
};
|
|
60
|
+
if (!closed) {
|
|
61
|
+
for (const part of input.intraFunction.parts) {
|
|
62
|
+
if (published(part.declaration)) {
|
|
63
|
+
unknown.add(part.declaration);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const found = [];
|
|
68
|
+
for (const part of input.intraFunction.parts) {
|
|
69
|
+
const declaration = byId.get(part.declaration);
|
|
70
|
+
const overlap = KINDS.get(part.code)?.overlap ?? [];
|
|
71
|
+
if (declaration === undefined || !reported(input, part, unknown)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
found.push({
|
|
75
|
+
code: part.code,
|
|
76
|
+
position: part.position,
|
|
77
|
+
symbol: {
|
|
78
|
+
ref: declaration.ref,
|
|
79
|
+
kind: part.kind,
|
|
80
|
+
name: part.name,
|
|
81
|
+
sizeLines: part.position.endLine - part.position.line + 1,
|
|
82
|
+
},
|
|
83
|
+
message: `${part.message}; ${overlapClause(overlap)}`,
|
|
84
|
+
details: {
|
|
85
|
+
...(part.kind === "store" ? { writePositions: [part.position] } : {}),
|
|
86
|
+
overlap,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return found;
|
|
91
|
+
};
|
|
@@ -2,6 +2,10 @@ import { REPOSITORY_DOCUMENT } from "../config.js";
|
|
|
2
2
|
import { STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
|
|
3
3
|
/** The code a configured root that matches nothing is reported under. */
|
|
4
4
|
const UNMATCHED_ROOT = "DS1704";
|
|
5
|
+
/** The code a configured declaration that names nothing is reported under. */
|
|
6
|
+
const UNMATCHED_DECLARATION = "DS1706";
|
|
7
|
+
/** The subject kind of a finding about a configured declaration. */
|
|
8
|
+
const DECLARATION_SUBJECT = "configured-declaration";
|
|
5
9
|
/** The subject kind of a finding about a suppression record. */
|
|
6
10
|
const SUPPRESSION_SUBJECT = "suppression";
|
|
7
11
|
/** The subject kind of a finding about a configured root. */
|
|
@@ -13,6 +17,7 @@ export const NO_SELF_CHECK = {
|
|
|
13
17
|
refusals: [],
|
|
14
18
|
unmatchedRoots: [],
|
|
15
19
|
rootsDocument: "",
|
|
20
|
+
unmatchedDeclarations: [],
|
|
16
21
|
};
|
|
17
22
|
/** The words a message names a mechanism by. */
|
|
18
23
|
const WRITTEN = {
|
|
@@ -104,6 +109,27 @@ export function staleSuppressions(stale, files) {
|
|
|
104
109
|
};
|
|
105
110
|
});
|
|
106
111
|
}
|
|
112
|
+
/** The first position of the document that configured a setting, or of the conventional one. */
|
|
113
|
+
function documentPosition(document) {
|
|
114
|
+
return {
|
|
115
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
116
|
+
line: DOCUMENT_POSITION,
|
|
117
|
+
column: DOCUMENT_POSITION,
|
|
118
|
+
endLine: DOCUMENT_POSITION,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One finding per configured declaration that named no declaration in any project, at the
|
|
123
|
+
* first position of the document that configured its key, as a configured root's is.
|
|
124
|
+
*/
|
|
125
|
+
function unmatchedDeclarationFindings(unmatched) {
|
|
126
|
+
return unmatched.map((one) => ({
|
|
127
|
+
code: UNMATCHED_DECLARATION,
|
|
128
|
+
position: documentPosition(one.document),
|
|
129
|
+
symbol: { ref: one.ref, kind: DECLARATION_SUBJECT, name: one.ref, sizeLines: 1 },
|
|
130
|
+
message: `${one.key} entry names no declaration in any project`,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
107
133
|
/**
|
|
108
134
|
* One finding per configured root or pattern that named nothing in any project. A finding
|
|
109
135
|
* names the document that declared the roots, or the conventional repository configuration
|
|
@@ -112,12 +138,7 @@ export function staleSuppressions(stale, files) {
|
|
|
112
138
|
* own, so the string each finding names tells one from another.
|
|
113
139
|
*/
|
|
114
140
|
export function unmatchedRootFindings(sources, document) {
|
|
115
|
-
const position =
|
|
116
|
-
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
117
|
-
line: DOCUMENT_POSITION,
|
|
118
|
-
column: DOCUMENT_POSITION,
|
|
119
|
-
endLine: DOCUMENT_POSITION,
|
|
120
|
-
};
|
|
141
|
+
const position = documentPosition(document);
|
|
121
142
|
return sources.map((source) => ({
|
|
122
143
|
code: UNMATCHED_ROOT,
|
|
123
144
|
position,
|
|
@@ -129,11 +150,13 @@ export function unmatchedRootFindings(sources, document) {
|
|
|
129
150
|
}
|
|
130
151
|
/**
|
|
131
152
|
* The findings of the self-check family, `DS1700` to `DS1799`: a suppression refused for
|
|
132
|
-
* carrying no reason or naming no path, and a configured root that named
|
|
153
|
+
* carrying no reason or naming no path, and a configured root or declaration that named
|
|
154
|
+
* nothing. A
|
|
133
155
|
* suppression that matched nothing is a record of the report's stale suppressions, which
|
|
134
156
|
* {@link staleSuppressions} builds, and a stale cross-language edge is the merge's to report.
|
|
135
157
|
*/
|
|
136
158
|
export const selfCheck = ({ selfCheck: facts, swept }) => [
|
|
137
159
|
...refusedSuppressions(facts.refusals, fileRefsOf(swept.matrix.union.symbols)),
|
|
138
160
|
...unmatchedRootFindings(facts.unmatchedRoots, facts.rootsDocument),
|
|
161
|
+
...unmatchedDeclarationFindings(facts.unmatchedDeclarations),
|
|
139
162
|
];
|
|
@@ -111,7 +111,15 @@ function messageOf(code, judged) {
|
|
|
111
111
|
return `${subject} has no reference in the target`;
|
|
112
112
|
}
|
|
113
113
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
114
|
-
export const unusedDeclarations = (input) =>
|
|
114
|
+
export const unusedDeclarations = (input) => familyFindings(input, false);
|
|
115
|
+
/**
|
|
116
|
+
* The finding each member of a dead container would carry were its container live: what
|
|
117
|
+
* a record on such a member is matched against, since the report carries the container's
|
|
118
|
+
* finding rather than one of the member's own.
|
|
119
|
+
*/
|
|
120
|
+
export const fallenMembers = (input) => familyFindings(input, true);
|
|
121
|
+
/** The family's findings, or, where `fallen` is set, its dead containers' members' alone. */
|
|
122
|
+
function familyFindings(input, fallen) {
|
|
115
123
|
const union = input.swept.matrix.union;
|
|
116
124
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
117
125
|
const unreachable = unreachableExports(input);
|
|
@@ -122,11 +130,15 @@ export const unusedDeclarations = (input) => {
|
|
|
122
130
|
if (symbol === undefined) {
|
|
123
131
|
continue;
|
|
124
132
|
}
|
|
133
|
+
const containerDead = dead.has(symbol.parent);
|
|
134
|
+
if (fallen !== containerDead) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
125
137
|
const judged = {
|
|
126
138
|
candidate,
|
|
127
139
|
symbol,
|
|
128
140
|
test: union.test[at] === true,
|
|
129
|
-
containerDead:
|
|
141
|
+
containerDead: containerDead && !fallen,
|
|
130
142
|
deprecated: input.deprecated.has(symbol.id),
|
|
131
143
|
unreachable: unreachable.has(symbol.id),
|
|
132
144
|
};
|
|
@@ -148,4 +160,4 @@ export const unusedDeclarations = (input) => {
|
|
|
148
160
|
});
|
|
149
161
|
}
|
|
150
162
|
return found;
|
|
151
|
-
}
|
|
163
|
+
}
|
|
@@ -76,18 +76,25 @@ export function verdictOf(result, config) {
|
|
|
76
76
|
return failing ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* The
|
|
80
|
-
* row of a document is not recorded: no record withholds one, so a row
|
|
81
|
-
* be stale on the first read back.
|
|
79
|
+
* The rows one round of a baseline write records from its findings, in their order. A
|
|
80
|
+
* finding about a row of a document is not recorded: no record withholds one, so a row
|
|
81
|
+
* recording it would be stale on the first read back. Two findings that would record the
|
|
82
|
+
* same row, two parts of one declaration under one code, record it once.
|
|
82
83
|
*/
|
|
83
|
-
export function recordedFindings(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
path: finding.position.path
|
|
90
|
-
|
|
84
|
+
export function recordedFindings(findings) {
|
|
85
|
+
const rows = new Map();
|
|
86
|
+
for (const finding of findings) {
|
|
87
|
+
if (isRowSubject(finding.symbol.kind)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
|
|
91
|
+
rows.set(rowKey(row), rows.get(rowKey(row)) ?? row);
|
|
92
|
+
}
|
|
93
|
+
return [...rows.values()];
|
|
94
|
+
}
|
|
95
|
+
/** One row's identity: its code, its symbol and its path. */
|
|
96
|
+
export function rowKey(row) {
|
|
97
|
+
return `${row.code}\u0000${row.symbol}\u0000${row.path}`;
|
|
91
98
|
}
|
|
92
99
|
/** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
|
|
93
100
|
export function analyzerProvenance(version) {
|
|
@@ -12,6 +12,7 @@ import { classMembers, memberComponent, typesAt } from "./class-members.js";
|
|
|
12
12
|
import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
|
|
13
13
|
import { nodeKey } from "./inventory.js";
|
|
14
14
|
import { renderPosition } from "./position.js";
|
|
15
|
+
import { must } from "./query.js";
|
|
15
16
|
/** Every contract's components and bases, resolved in one pass over the entries. */
|
|
16
17
|
function resolveContracts(project, held, contracts) {
|
|
17
18
|
const entries = contracts.flatMap((contract) => [
|
|
@@ -106,7 +107,7 @@ function byBases(project, held, chains, contracts) {
|
|
|
106
107
|
if (classes.length === 0) {
|
|
107
108
|
return [];
|
|
108
109
|
}
|
|
109
|
-
const
|
|
110
|
+
const queries = project.queries;
|
|
110
111
|
const ancestry = new Map();
|
|
111
112
|
const ancestorsOf = (type, seen) => {
|
|
112
113
|
const known = ancestry.get(type.id);
|
|
@@ -116,15 +117,16 @@ function byBases(project, held, chains, contracts) {
|
|
|
116
117
|
const declared = type.isClassOrInterface()
|
|
117
118
|
? type
|
|
118
119
|
: type.isTypeReference()
|
|
119
|
-
?
|
|
120
|
+
? must(queries.targetOf(type))
|
|
120
121
|
: undefined;
|
|
121
122
|
if (declared === undefined || !declared.isClassOrInterface() || seen.has(declared.id)) {
|
|
122
123
|
return [];
|
|
123
124
|
}
|
|
124
125
|
seen.add(declared.id);
|
|
125
|
-
const found =
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const found = must(queries.baseTypes(declared)).flatMap((base) => [
|
|
127
|
+
base,
|
|
128
|
+
...ancestorsOf(base, seen),
|
|
129
|
+
]);
|
|
128
130
|
ancestry.set(type.id, found);
|
|
129
131
|
return found;
|
|
130
132
|
};
|
|
@@ -135,7 +137,7 @@ function byBases(project, held, chains, contracts) {
|
|
|
135
137
|
if (type === undefined) {
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
|
-
const ancestors = ancestorsOf(type, new Set()).flatMap((base) =>
|
|
140
|
+
const ancestors = ancestorsOf(type, new Set()).flatMap((base) => must(queries.symbolOfType(base)) ?? []);
|
|
139
141
|
for (const contract of based) {
|
|
140
142
|
for (const entry of contract.bases) {
|
|
141
143
|
if (ancestors.some((symbol) => names(entry, symbol, chains))) {
|
|
@@ -9,7 +9,7 @@ import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodD
|
|
|
9
9
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
10
|
import { aliasChains } from "./alias-chain.js";
|
|
11
11
|
import { nodeKey } from "./inventory.js";
|
|
12
|
-
import {
|
|
12
|
+
import { must, Unanswerable } from "./query.js";
|
|
13
13
|
/** The interfaces, the classes and the `implements` entries of one project's own files. */
|
|
14
14
|
function walk(project, held) {
|
|
15
15
|
const interfaces = [];
|
|
@@ -41,23 +41,21 @@ function walk(project, held) {
|
|
|
41
41
|
}
|
|
42
42
|
return { interfaces, classes, clauses };
|
|
43
43
|
}
|
|
44
|
-
/** The type at each node
|
|
44
|
+
/** The type at each node. */
|
|
45
45
|
function typesAt(project, nodes) {
|
|
46
46
|
const typeAt = new Map();
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
}
|
|
47
|
+
project.queries.typesAt(nodes).forEach((answer, index) => {
|
|
48
|
+
const type = must(answer);
|
|
49
|
+
const node = nodes[index];
|
|
50
|
+
if (type !== undefined && node !== undefined) {
|
|
51
|
+
typeAt.set(node, type);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
56
54
|
return typeAt;
|
|
57
55
|
}
|
|
58
56
|
/** Whether one class member is a method whose every written body holds no statement. */
|
|
59
57
|
function emptyImplementation(project, member) {
|
|
60
|
-
const own =
|
|
58
|
+
const own = project.ownPaths();
|
|
61
59
|
let bodies = 0;
|
|
62
60
|
for (const handle of member.declarations) {
|
|
63
61
|
// A body outside the target is one this analysis cannot read, so it is not empty.
|
|
@@ -77,15 +75,32 @@ function emptyImplementation(project, member) {
|
|
|
77
75
|
}
|
|
78
76
|
return bodies > 0;
|
|
79
77
|
}
|
|
80
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* How one project's interfaces are implemented by its classes. Where the checker leaves
|
|
80
|
+
* a question unanswered, what was read stands and every interface method of the project
|
|
81
|
+
* is named in {@link Implementations.unknown}.
|
|
82
|
+
*/
|
|
81
83
|
export function implementations(project, held) {
|
|
82
84
|
const classes = new Map();
|
|
83
85
|
const bodies = new Map();
|
|
86
|
+
try {
|
|
87
|
+
implementedIn(project, held, classes, bodies);
|
|
88
|
+
return { classes, bodies, unknown: new Set() };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!(error instanceof Unanswerable)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const methods = held.symbols.filter((symbol) => symbol.kind === "interface-method");
|
|
95
|
+
return { classes, bodies, unknown: new Set(methods.map((symbol) => symbol.id)) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function implementedIn(project, held, classes, bodies) {
|
|
84
99
|
const walked = walk(project, held);
|
|
85
100
|
if (walked.interfaces.length === 0 || walked.classes.length === 0) {
|
|
86
|
-
return
|
|
101
|
+
return;
|
|
87
102
|
}
|
|
88
|
-
const
|
|
103
|
+
const queries = project.queries;
|
|
89
104
|
const typeAt = typesAt(project, [
|
|
90
105
|
...walked.interfaces.map((one) => one.nameNode),
|
|
91
106
|
...walked.classes.map((one) => one.nameNode),
|
|
@@ -114,8 +129,8 @@ export function implementations(project, held) {
|
|
|
114
129
|
classAt.set(owner.id, { owner, type });
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
|
-
const membersOf = (at) => (at.members ??=
|
|
118
|
-
const classMembers = (at) => (at.members ??= new Map(
|
|
132
|
+
const membersOf = (at) => (at.members ??= must(queries.propertiesOf(at.type)));
|
|
133
|
+
const classMembers = (at) => (at.members ??= new Map(must(queries.propertiesOf(at.type)).map((member) => [member.escapedName, member])));
|
|
119
134
|
const methodIds = new Set(held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id));
|
|
120
135
|
const chains = aliasChains(project, held);
|
|
121
136
|
const record = (at, implementer) => {
|
|
@@ -161,11 +176,13 @@ export function implementations(project, held) {
|
|
|
161
176
|
if (entry === undefined || implementer === undefined) {
|
|
162
177
|
continue;
|
|
163
178
|
}
|
|
164
|
-
const at = byType.get(
|
|
179
|
+
const at = byType.get(must(queries.targetOf(entry)).id);
|
|
165
180
|
if (at !== undefined) {
|
|
166
181
|
pair(at, implementer);
|
|
167
182
|
}
|
|
168
183
|
}
|
|
184
|
+
// Every pair whose names line up is asked about at once.
|
|
185
|
+
const candidates = [];
|
|
169
186
|
for (const at of byType.values()) {
|
|
170
187
|
const members = at.generic ? [] : membersOf(at);
|
|
171
188
|
if (members.length === 0) {
|
|
@@ -178,16 +195,21 @@ export function implementations(project, held) {
|
|
|
178
195
|
}
|
|
179
196
|
const written = classMembers(implementer);
|
|
180
197
|
if (required.every((member) => written.has(member.escapedName)) &&
|
|
181
|
-
members.some((member) => written.has(member.escapedName))
|
|
182
|
-
|
|
183
|
-
pair(at, implementer);
|
|
198
|
+
members.some((member) => written.has(member.escapedName))) {
|
|
199
|
+
candidates.push({ at, implementer });
|
|
184
200
|
}
|
|
185
201
|
}
|
|
186
202
|
}
|
|
187
|
-
|
|
203
|
+
const assignable = queries.assignableEach(candidates.map((one) => ({ source: one.implementer.type, target: one.at.type })));
|
|
204
|
+
candidates.forEach((one, index) => {
|
|
205
|
+
if (must(assignable[index] ?? false)) {
|
|
206
|
+
pair(one.at, one.implementer);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
188
209
|
}
|
|
189
210
|
/** Every project's answers as one run's, each implementation once. */
|
|
190
211
|
export function mergeImplementations(per) {
|
|
212
|
+
const unknown = new Set(per.flatMap((one) => [...one.unknown]));
|
|
191
213
|
const classes = new Map();
|
|
192
214
|
const bodies = new Map();
|
|
193
215
|
for (const one of per) {
|
|
@@ -206,5 +228,5 @@ export function mergeImplementations(per) {
|
|
|
206
228
|
}
|
|
207
229
|
}
|
|
208
230
|
}
|
|
209
|
-
return { classes, bodies };
|
|
231
|
+
return { classes, bodies, unknown };
|
|
210
232
|
}
|
|
@@ -17,7 +17,7 @@ import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
|
17
17
|
import { aliasChains } from "./alias-chain.js";
|
|
18
18
|
import { nodeKey } from "./inventory.js";
|
|
19
19
|
import { renderPosition } from "./position.js";
|
|
20
|
-
import {
|
|
20
|
+
import { must } from "./query.js";
|
|
21
21
|
/** The operators of an assignment whose right side is stored into the left side's type. */
|
|
22
22
|
const STORES = new Set([
|
|
23
23
|
SyntaxKind.EqualsToken,
|
|
@@ -157,7 +157,7 @@ function walk(files, declarations) {
|
|
|
157
157
|
*/
|
|
158
158
|
export function interfaceSatisfaction(input) {
|
|
159
159
|
const { project, held, targetRoot } = input;
|
|
160
|
-
const
|
|
160
|
+
const queries = project.queries;
|
|
161
161
|
const files = project.ownSourceFiles();
|
|
162
162
|
const { classes, clauses, flows } = walk(files, held.declarations);
|
|
163
163
|
if (classes.length === 0) {
|
|
@@ -171,15 +171,13 @@ export function interfaceSatisfaction(input) {
|
|
|
171
171
|
...flows.filter((flow) => flow.self === undefined).map((flow) => flow.value),
|
|
172
172
|
];
|
|
173
173
|
const typeAt = new Map();
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
}
|
|
174
|
+
queries.typesAt(asked).forEach((answer, index) => {
|
|
175
|
+
const type = must(answer);
|
|
176
|
+
const node = asked[index];
|
|
177
|
+
if (type !== undefined && node !== undefined) {
|
|
178
|
+
typeAt.set(node, type);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
183
181
|
const classOfType = new Map();
|
|
184
182
|
const instanceOf = new Map();
|
|
185
183
|
for (const owner of classes) {
|
|
@@ -197,7 +195,7 @@ export function interfaceSatisfaction(input) {
|
|
|
197
195
|
}
|
|
198
196
|
let parts = constituents.get(type.id);
|
|
199
197
|
if (parts === undefined) {
|
|
200
|
-
parts =
|
|
198
|
+
parts = must(queries.constituents(type));
|
|
201
199
|
constituents.set(type.id, parts);
|
|
202
200
|
}
|
|
203
201
|
return parts;
|
|
@@ -213,7 +211,7 @@ export function interfaceSatisfaction(input) {
|
|
|
213
211
|
return [];
|
|
214
212
|
}
|
|
215
213
|
if (!targets.has(part.id)) {
|
|
216
|
-
targets.set(part.id, classOfType.get(
|
|
214
|
+
targets.set(part.id, classOfType.get(must(queries.targetOf(part)).id));
|
|
217
215
|
}
|
|
218
216
|
const generic = targets.get(part.id);
|
|
219
217
|
return generic === undefined ? [] : [{ owner: generic, type: part }];
|
|
@@ -222,7 +220,7 @@ export function interfaceSatisfaction(input) {
|
|
|
222
220
|
/** The interface types one position's type is or holds, each with the interface's name. */
|
|
223
221
|
const interfacesOf = (type) => partsOf(type).flatMap((part) => {
|
|
224
222
|
if (!interfaceNames.has(part.id)) {
|
|
225
|
-
const symbol = part.isObjectType() ?
|
|
223
|
+
const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
|
|
226
224
|
interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
|
|
227
225
|
? symbol.name
|
|
228
226
|
: undefined);
|
|
@@ -251,7 +249,8 @@ export function interfaceSatisfaction(input) {
|
|
|
251
249
|
reach(clause.owner, source, reached.type, reached.name, clause.entry);
|
|
252
250
|
}
|
|
253
251
|
}
|
|
254
|
-
|
|
252
|
+
// The positions of every flow carrying a class are asked for at once.
|
|
253
|
+
const carrying = flows.flatMap((flow) => {
|
|
255
254
|
const self = flow.self === undefined ? undefined : instanceOf.get(flow.self.id);
|
|
256
255
|
const valueType = typeAt.get(flow.value);
|
|
257
256
|
const sources = flow.self !== undefined && self !== undefined
|
|
@@ -259,38 +258,42 @@ export function interfaceSatisfaction(input) {
|
|
|
259
258
|
: valueType === undefined
|
|
260
259
|
? []
|
|
261
260
|
: classesOf(valueType);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
return sources.length === 0 ? [] : [{ flow, sources }];
|
|
262
|
+
});
|
|
263
|
+
const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
|
|
264
|
+
carrying.forEach(({ flow, sources }, index) => {
|
|
265
|
+
const position = must(positions[index]);
|
|
266
266
|
if (position === undefined) {
|
|
267
|
-
|
|
267
|
+
return;
|
|
268
268
|
}
|
|
269
269
|
for (const reached of interfacesOf(position)) {
|
|
270
270
|
for (const source of sources) {
|
|
271
271
|
reach(source.owner, source.type, reached.type, reached.name, flow.value);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
}
|
|
274
|
+
});
|
|
275
275
|
const chains = aliasChains(project, held);
|
|
276
276
|
const required = new Map();
|
|
277
277
|
const members = new Map();
|
|
278
278
|
const found = [];
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
const asking = [...pairs.values()];
|
|
280
|
+
const assignable = queries.assignableEach(asking);
|
|
281
|
+
for (const [index, pair] of asking.entries()) {
|
|
282
|
+
if (!must(assignable[index] ?? false)) {
|
|
281
283
|
continue;
|
|
282
284
|
}
|
|
283
285
|
let names = required.get(pair.target.id);
|
|
284
286
|
if (names === undefined) {
|
|
285
|
-
names = new Set(
|
|
287
|
+
names = new Set(must(queries.propertiesOf(pair.target)).map((property) => property.escapedName));
|
|
286
288
|
required.set(pair.target.id, names);
|
|
287
289
|
}
|
|
288
290
|
let answering = members.get(pair.owner.id);
|
|
289
291
|
if (answering === undefined) {
|
|
290
292
|
const instance = instanceOf.get(pair.owner.id) ?? pair.source;
|
|
291
|
-
answering = new Map(
|
|
292
|
-
.
|
|
293
|
-
|
|
293
|
+
answering = new Map(must(queries.propertiesOf(instance)).map((property) => [
|
|
294
|
+
property.escapedName,
|
|
295
|
+
chains.declarationsOf(property),
|
|
296
|
+
]));
|
|
294
297
|
members.set(pair.owner.id, answering);
|
|
295
298
|
}
|
|
296
299
|
const detail = `satisfies ${pair.name}`;
|