@cplieger/deadset-ts 3.13.0 → 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 +3 -3
- package/dist/bin/node-host.js +10 -7
- package/dist/src/alias-chain.js +59 -7
- package/dist/src/analysis.js +119 -23
- package/dist/src/calls.js +23 -22
- package/dist/src/class-members.js +10 -11
- package/dist/src/configured-declarations.js +22 -9
- 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/entry-points.js +7 -0
- package/dist/src/enum-group.js +46 -40
- package/dist/src/exempt.js +42 -1
- package/dist/src/findings/emitters.js +15 -1
- package/dist/src/findings/interfaces.js +7 -1
- 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 +37 -20
- package/dist/src/inventory.js +85 -4
- package/dist/src/json-chunks.js +76 -0
- 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 +18 -9
- package/dist/src/report.js +3 -1
- package/dist/src/reporters/annotations.js +9 -5
- package/dist/src/reporters/json.js +3 -2
- package/dist/src/reporters/sarif.js +2 -1
- package/dist/src/reporters/template.js +48 -7
- package/dist/src/reporters/text.js +8 -6
- package/dist/src/roots.js +27 -11
- package/dist/src/serialization-contract.js +12 -10
- package/dist/src/session.js +79 -39
- package/dist/src/suppress-file.js +2 -1
- package/dist/src/sweep.js +4 -1
- package/dist/src/value-reach.js +14 -19
- package/dist/src/verbs/analyze.js +20 -3
- 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/jsr.json +1 -1
- package/package.json +3 -3
- package/src/alias-chain.ts +75 -11
- package/src/analysis.ts +223 -61
- package/src/calls.ts +10 -9
- package/src/class-members.ts +10 -11
- package/src/configured-declarations.ts +29 -13
- 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/entry-points.ts +7 -0
- package/src/enum-group.ts +56 -42
- package/src/exempt.ts +50 -2
- package/src/findings/emitters.ts +18 -1
- package/src/findings/interfaces.ts +7 -1
- 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 +43 -25
- package/src/inventory.ts +97 -4
- package/src/json-chunks.ts +87 -0
- package/src/partial.ts +40 -0
- package/src/query.ts +522 -0
- package/src/references.ts +119 -38
- package/src/reflective-lookup.ts +21 -11
- package/src/report.ts +14 -2
- 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 +3 -2
- package/src/reporters/template.ts +50 -8
- package/src/reporters/text.ts +12 -12
- package/src/roots.ts +34 -12
- package/src/serialization-contract.ts +12 -10
- package/src/session.ts +146 -58
- package/src/suppress-file.ts +6 -2
- package/src/sweep.ts +9 -4
- package/src/value-reach.ts +20 -23
- package/src/verbs/analyze.ts +25 -3
- 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/dist/src/enum-group.js
CHANGED
|
@@ -11,7 +11,7 @@ import { TypeFlags } from "@typescript/native/unstable/sync";
|
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
12
|
import { nodeKey } from "./inventory.js";
|
|
13
13
|
import { renderPosition } from "./position.js";
|
|
14
|
-
import {
|
|
14
|
+
import { isAnswered, UNANSWERED } from "./query.js";
|
|
15
15
|
/** The operand types a conversion produces an enum value from. */
|
|
16
16
|
const NUMBER = TypeFlags.NumberLike;
|
|
17
17
|
const STRING = TypeFlags.StringLike;
|
|
@@ -104,24 +104,31 @@ function sitesOf(file) {
|
|
|
104
104
|
* union, and itself otherwise. An enum's own value, or one of its members, is a value the
|
|
105
105
|
* source names, whichever number or string it also is.
|
|
106
106
|
*/
|
|
107
|
-
function partsOf(type) {
|
|
108
|
-
const parts = type.isUnionType() ?
|
|
109
|
-
return parts
|
|
107
|
+
function partsOf(project, type) {
|
|
108
|
+
const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
|
|
109
|
+
return isAnswered(parts)
|
|
110
|
+
? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
|
|
111
|
+
: parts;
|
|
110
112
|
}
|
|
113
|
+
/** The detail a site records whose operand's type the checker could not answer. */
|
|
114
|
+
const UNTYPED_DETAIL = "converted from a value of unanswered type";
|
|
111
115
|
/**
|
|
112
116
|
* The detail one site records about the enum it produces, or undefined where the
|
|
113
117
|
* operand's type converts nothing: a number, or a string an assertion converts, is
|
|
114
118
|
* converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
|
|
115
119
|
* the source names.
|
|
116
120
|
*/
|
|
117
|
-
function detailOf(site, enumName, operand) {
|
|
121
|
+
function detailOf(project, site, enumName, operand) {
|
|
118
122
|
if (site.form === "lookup") {
|
|
119
123
|
return `looked up by an element access on ${enumName}`;
|
|
120
124
|
}
|
|
121
125
|
if (operand === undefined) {
|
|
122
126
|
return undefined;
|
|
123
127
|
}
|
|
124
|
-
const parts = partsOf(operand);
|
|
128
|
+
const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
|
|
129
|
+
if (parts === UNANSWERED) {
|
|
130
|
+
return UNTYPED_DETAIL;
|
|
131
|
+
}
|
|
125
132
|
if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
|
|
126
133
|
return "converted from number";
|
|
127
134
|
}
|
|
@@ -160,32 +167,33 @@ export function enumGroup(input) {
|
|
|
160
167
|
const sites = files.flatMap(sitesOf);
|
|
161
168
|
const named = new Map();
|
|
162
169
|
const chains = aliasChains(project, held);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// A name the checker could not resolve may stand for any enum of the target it spells.
|
|
171
|
+
const enumsNamed = (text) => [...enums].flatMap(([id, name]) => (name === text ? [id] : []));
|
|
172
|
+
project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
|
|
173
|
+
const site = sites[index];
|
|
174
|
+
if (symbol === undefined || site === undefined) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (symbol === UNANSWERED) {
|
|
178
|
+
const ids = enumsNamed(site.name.getText());
|
|
179
|
+
if (ids.length > 0) {
|
|
180
|
+
named.set(site, ids);
|
|
173
181
|
}
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
|
|
185
|
+
if (target !== undefined) {
|
|
186
|
+
named.set(site, [target.id]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
176
189
|
const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
|
|
177
190
|
const operandType = new Map();
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (type !== undefined && site !== undefined) {
|
|
185
|
-
operandType.set(site, type);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
191
|
+
project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
|
|
192
|
+
const site = typed[index];
|
|
193
|
+
if (type !== undefined && site !== undefined) {
|
|
194
|
+
operandType.set(site, type);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
189
197
|
const members = new Map();
|
|
190
198
|
for (const symbol of held.symbols) {
|
|
191
199
|
if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
|
|
@@ -200,17 +208,15 @@ export function enumGroup(input) {
|
|
|
200
208
|
}
|
|
201
209
|
const found = [];
|
|
202
210
|
for (const site of sites) {
|
|
203
|
-
const enumId
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const id of members.get(enumId) ?? []) {
|
|
213
|
-
found.push({ id, detail, site: position });
|
|
211
|
+
for (const enumId of named.get(site) ?? []) {
|
|
212
|
+
const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
|
|
213
|
+
if (detail === undefined) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
|
|
217
|
+
for (const id of members.get(enumId) ?? []) {
|
|
218
|
+
found.push({ id, detail, site: position });
|
|
219
|
+
}
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
return found;
|
package/dist/src/exempt.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { ConfigError } from "./config.js";
|
|
10
10
|
import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
|
|
11
11
|
import { byPosition, positionKey } from "./position.js";
|
|
12
|
+
import { Unanswerable } from "./query.js";
|
|
12
13
|
import { sweep } from "./sweep.js";
|
|
13
14
|
/**
|
|
14
15
|
* The classes `exemptions.disabled` switches off. A name is refused unless the
|
|
@@ -39,6 +40,46 @@ function retainable(allowed, visibility) {
|
|
|
39
40
|
}
|
|
40
41
|
return true;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The kinds of declaration each class can hold back, which a detector that stops holds
|
|
45
|
+
* back whole. A class with no row here holds back every declaration but a file.
|
|
46
|
+
*/
|
|
47
|
+
const RETAINABLE = new Map([
|
|
48
|
+
["interface-satisfaction", new Set(["method", "class-member"])],
|
|
49
|
+
["enum-group", new Set(["enum-member"])],
|
|
50
|
+
["injection-container", new Set(["method", "class-member"])],
|
|
51
|
+
["framework-lifecycle", new Set(["method", "class-member"])],
|
|
52
|
+
[
|
|
53
|
+
"serialization-contract",
|
|
54
|
+
new Set(["method", "class-member", "interface-method", "type-member"]),
|
|
55
|
+
],
|
|
56
|
+
]);
|
|
57
|
+
/** The detail of a record a detector that could not finish holds. */
|
|
58
|
+
const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
|
|
59
|
+
/** Whether one record is held by a detector that could not finish. */
|
|
60
|
+
export function isUnansweredRecord(record) {
|
|
61
|
+
return record.detail === UNANSWERED_DETAIL;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What one detector holds back over one project. A detector that stops because the
|
|
65
|
+
* checker left a question it needs unanswered holds back every declaration of the
|
|
66
|
+
* project its class could hold back, each recorded at its own position, so the gap
|
|
67
|
+
* never yields a finding.
|
|
68
|
+
*/
|
|
69
|
+
function detected(exemptionClass, detect, input) {
|
|
70
|
+
try {
|
|
71
|
+
return detect(input);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!(error instanceof Unanswerable)) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const kinds = RETAINABLE.get(exemptionClass);
|
|
78
|
+
return input.held.symbols
|
|
79
|
+
.filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
|
|
80
|
+
.map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
42
83
|
/**
|
|
43
84
|
* The records one project's detectors found, each class in the vocabulary's order and
|
|
44
85
|
* none of a switched-off class. Evidence on a private member the class's row says it
|
|
@@ -55,7 +96,7 @@ export function computeExemptions(input, detectors, holding) {
|
|
|
55
96
|
continue;
|
|
56
97
|
}
|
|
57
98
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
58
|
-
for (const evidence of detect
|
|
99
|
+
for (const evidence of detected(exemptionClass, detect, input)) {
|
|
59
100
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
60
101
|
continue;
|
|
61
102
|
}
|
|
@@ -20,6 +20,19 @@ export const EMITTERS = new Map([
|
|
|
20
20
|
["self-check", selfCheck],
|
|
21
21
|
["intra-function", intraFunction],
|
|
22
22
|
]);
|
|
23
|
+
/**
|
|
24
|
+
* The references of every declaration an unanswered question could have kept live, which
|
|
25
|
+
* the run reports nothing about: a finding about one would be computed from the gap.
|
|
26
|
+
*/
|
|
27
|
+
function heldRefs(input) {
|
|
28
|
+
const held = new Set(input.swept.heldByUnanswered);
|
|
29
|
+
if (held.size === 0) {
|
|
30
|
+
return held;
|
|
31
|
+
}
|
|
32
|
+
return new Set(input.swept.matrix.union.symbols
|
|
33
|
+
.filter((symbol) => held.has(symbol.id))
|
|
34
|
+
.map((symbol) => symbol.ref));
|
|
35
|
+
}
|
|
23
36
|
/** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
|
|
24
37
|
function completedOver(input, ...more) {
|
|
25
38
|
return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
|
|
@@ -50,7 +63,8 @@ export function decidedFindings(input, suppressed = { unmarked: input, records:
|
|
|
50
63
|
const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
|
|
51
64
|
const would = [...unmarked, ...found];
|
|
52
65
|
const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
|
|
53
|
-
const
|
|
66
|
+
const unanswered = heldRefs(input);
|
|
67
|
+
const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)));
|
|
54
68
|
const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
|
|
55
69
|
return {
|
|
56
70
|
findings: evaluated.findings,
|
|
@@ -88,8 +88,14 @@ function unusedMessage(candidate) {
|
|
|
88
88
|
? UNUSED_TEST_MESSAGE
|
|
89
89
|
: UNUSED_MESSAGE;
|
|
90
90
|
}
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Whether one interface method has implementations and every one has an empty body, or
|
|
93
|
+
* is one whose implementations the run could not read in full.
|
|
94
|
+
*/
|
|
92
95
|
function marker(input, method) {
|
|
96
|
+
if (input.implementations.unknown.has(method)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
93
99
|
const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
|
|
94
100
|
return bodies.length > 0 && bodies.every((empty) => empty);
|
|
95
101
|
}
|
|
@@ -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}`;
|