@cplieger/deadset-ts 3.5.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/dist/src/analysis.js +70 -14
- package/dist/src/components.js +5 -1
- 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/resolve.js +1 -1
- package/dist/src/stores.js +32 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +113 -16
- package/src/components.ts +6 -1
- 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/resolve.ts +1 -1
- package/src/stores.ts +77 -0
|
@@ -1,2 +1,239 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { OUTSIDE } from "../graph.js";
|
|
2
|
+
import { writeOnlyDeclarations } from "./reads-and-writes.js";
|
|
3
|
+
const UNNECESSARY_EXPORT = "DS1101";
|
|
4
|
+
const UNREACHABLE_EXPORT = "DS1103";
|
|
5
|
+
const REDUNDANT_EXPORT_KEYWORD = "DS1104";
|
|
6
|
+
/**
|
|
7
|
+
* The root kinds by which a consumer importing the package reaches a declaration. A
|
|
8
|
+
* declaration or a file rooted by any other kind is entered by something that names it
|
|
9
|
+
* from outside the reference graph: a runtime, a tool, a framework or the configuration.
|
|
10
|
+
*/
|
|
11
|
+
const PUBLISHING = new Set(["manifest-entry", "published-api"]);
|
|
12
|
+
/** The reader's word for each kind of declaration this family reports. */
|
|
13
|
+
const WORDS = {
|
|
14
|
+
function: "function",
|
|
15
|
+
class: "class",
|
|
16
|
+
interface: "interface",
|
|
17
|
+
type: "type alias",
|
|
18
|
+
enum: "enum",
|
|
19
|
+
namespace: "namespace",
|
|
20
|
+
variable: "variable",
|
|
21
|
+
"export-alias": "re-export",
|
|
22
|
+
};
|
|
23
|
+
function publishes(kinds) {
|
|
24
|
+
return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
|
|
25
|
+
}
|
|
26
|
+
function entersByName(kinds) {
|
|
27
|
+
return [...(kinds ?? [])].some((kind) => !PUBLISHING.has(kind));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Folds the run's references into the reach of each declaration.
|
|
31
|
+
*
|
|
32
|
+
* A reference reaches the file that holds the declaration that makes it. A reference
|
|
33
|
+
* from a declaration the inventory does not hold is made by a consumer, and one from a
|
|
34
|
+
* re-export a manifest export reaches stands for every consumer importing the name it
|
|
35
|
+
* publishes; either reaches outside the package.
|
|
36
|
+
*/
|
|
37
|
+
function factsOf(graph) {
|
|
38
|
+
const { symbols, parent } = graph;
|
|
39
|
+
const fileOf = symbols.map((_symbol, at) => {
|
|
40
|
+
let file = at;
|
|
41
|
+
while (file !== OUTSIDE && symbols[file]?.kind !== "file") {
|
|
42
|
+
file = parent[file] ?? OUTSIDE;
|
|
43
|
+
}
|
|
44
|
+
return file;
|
|
45
|
+
});
|
|
46
|
+
const rootedBy = symbols.map(() => new Set());
|
|
47
|
+
for (const root of graph.rooted) {
|
|
48
|
+
rootedBy[root.at]?.add(root.kind);
|
|
49
|
+
}
|
|
50
|
+
const published = symbols.map(() => false);
|
|
51
|
+
symbols.forEach((_symbol, at) => {
|
|
52
|
+
const file = fileOf[at] ?? OUTSIDE;
|
|
53
|
+
if (file !== OUTSIDE && publishes(rootedBy[at])) {
|
|
54
|
+
published[file] = true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const order = ["file", "package", "outside"];
|
|
58
|
+
const wider = (a, b) => (order.indexOf(a) >= order.indexOf(b) ? a : b);
|
|
59
|
+
const inGraph = symbols.map(() => 0);
|
|
60
|
+
const reach = symbols.map(() => "file");
|
|
61
|
+
graph.out.forEach((edges, from) => {
|
|
62
|
+
const republished = symbols[from]?.kind === "export-alias" && publishes(rootedBy[from]);
|
|
63
|
+
for (const edge of edges) {
|
|
64
|
+
inGraph[edge.to] = (inGraph[edge.to] ?? 0) + 1;
|
|
65
|
+
const scope = republished
|
|
66
|
+
? "outside"
|
|
67
|
+
: fileOf[from] === fileOf[edge.to]
|
|
68
|
+
? "file"
|
|
69
|
+
: "package";
|
|
70
|
+
reach[edge.to] = wider(reach[edge.to] ?? "file", scope);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const named = graph.made.map((counts) => counts.production + counts.test);
|
|
74
|
+
named.forEach((count, at) => {
|
|
75
|
+
if (count > (inGraph[at] ?? 0)) {
|
|
76
|
+
reach[at] = "outside";
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return { graph, fileOf, rootedBy, published, named, reach };
|
|
80
|
+
}
|
|
81
|
+
/** Whether one declaration is a declaration its file's export table names. */
|
|
82
|
+
function topLevelExport(facts, at, symbol) {
|
|
83
|
+
const parent = facts.graph.parent[at] ?? OUTSIDE;
|
|
84
|
+
return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
|
|
85
|
+
}
|
|
86
|
+
function wordOf(symbol) {
|
|
87
|
+
return WORDS[symbol.kind] ?? symbol.kind;
|
|
88
|
+
}
|
|
89
|
+
/** The visibility each narrowing code names, which only the narrowing codes carry. */
|
|
90
|
+
const NARROWER = {
|
|
91
|
+
[REDUNDANT_EXPORT_KEYWORD]: "file",
|
|
92
|
+
[UNNECESSARY_EXPORT]: "package",
|
|
93
|
+
};
|
|
94
|
+
function findingOf(symbol, code, message) {
|
|
95
|
+
const { path, line, column } = symbol.position;
|
|
96
|
+
const narrower = Object.hasOwn(NARROWER, code) ? NARROWER[code] : undefined;
|
|
97
|
+
const endLine = Math.max(line, symbol.endLine);
|
|
98
|
+
return {
|
|
99
|
+
code,
|
|
100
|
+
position: { path, line, column, endLine },
|
|
101
|
+
symbol: {
|
|
102
|
+
ref: symbol.ref,
|
|
103
|
+
kind: symbol.kind,
|
|
104
|
+
name: symbol.name,
|
|
105
|
+
sizeLines: endLine - line + 1,
|
|
106
|
+
},
|
|
107
|
+
message,
|
|
108
|
+
details: narrower === undefined ? {} : { narrowerVisibility: narrower },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The narrowing finding about one live declaration, if one holds: an export of a file,
|
|
113
|
+
* no re-export and none the write-only kind reports, that a reference names and nothing
|
|
114
|
+
* enters by name. Every reference inside its own file makes it a redundant export
|
|
115
|
+
* keyword, where no manifest export reaches the file or the world is closed; references
|
|
116
|
+
* from other files of the package alone make a published one unnecessary when closed.
|
|
117
|
+
*/
|
|
118
|
+
function narrowing(facts, at, symbol, world) {
|
|
119
|
+
const { closed } = world;
|
|
120
|
+
const file = facts.fileOf[at] ?? OUTSIDE;
|
|
121
|
+
if (!topLevelExport(facts, at, symbol) ||
|
|
122
|
+
world.writeOnly.has(symbol.ref) ||
|
|
123
|
+
symbol.kind === "export-alias" ||
|
|
124
|
+
entersByName(facts.rootedBy[at]) ||
|
|
125
|
+
entersByName(facts.rootedBy[file]) ||
|
|
126
|
+
(facts.named[at] ?? 0) === 0) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const word = wordOf(symbol);
|
|
130
|
+
switch (facts.reach[at]) {
|
|
131
|
+
case "file":
|
|
132
|
+
if (facts.published[file] === true && !closed) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return findingOf(symbol, REDUNDANT_EXPORT_KEYWORD, `exported ${word} is referenced only inside the file that declares it`);
|
|
136
|
+
case "package":
|
|
137
|
+
if (!closed || !publishes(facts.rootedBy[at])) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return findingOf(symbol, UNNECESSARY_EXPORT, `exported ${word} is referenced only inside the package that declares it`);
|
|
141
|
+
default:
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The unreachable-export finding about one dead declaration, if one holds: an unused
|
|
147
|
+
* export no code outside the target can import, because the manifest declares
|
|
148
|
+
* `exports` and no root reaches it. Each arm yields to a more specific kind, so one
|
|
149
|
+
* declaration is reported once: a test whose every subject is dead, an interface, a
|
|
150
|
+
* declaration only a test file references, and a deprecated one.
|
|
151
|
+
*/
|
|
152
|
+
function unreachable(facts, at, symbol, candidate, input) {
|
|
153
|
+
if (!input.boundary.encapsulated ||
|
|
154
|
+
!topLevelExport(facts, at, symbol) ||
|
|
155
|
+
(facts.rootedBy[at]?.size ?? 0) > 0 ||
|
|
156
|
+
candidate.testOfDeadCode ||
|
|
157
|
+
symbol.kind === "interface" ||
|
|
158
|
+
(candidate.productionRefs === 0 && candidate.testRefs > 0) ||
|
|
159
|
+
(candidate.productionRefs === 0 && input.deprecated.has(symbol.id))) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const found = candidate.relation === "reachability"
|
|
163
|
+
? "is referenced only from declarations that are themselves dead"
|
|
164
|
+
: "has no reference in the target";
|
|
165
|
+
return findingOf(symbol, UNREACHABLE_EXPORT, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
|
|
166
|
+
}
|
|
167
|
+
/** Two strings ordered bytewise. */
|
|
168
|
+
function compare(a, b) {
|
|
169
|
+
if (a === b) {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
return a < b ? -1 : 1;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`.
|
|
176
|
+
*
|
|
177
|
+
* A finding about a symbol a declared cross-language edge names is pending instead of
|
|
178
|
+
* reported, once per side naming it: the edge counts as a reference from outside the
|
|
179
|
+
* file and the package, and whether it is a use is decided on the side in the other
|
|
180
|
+
* language.
|
|
181
|
+
*/
|
|
182
|
+
export function narrowings(input) {
|
|
183
|
+
const { boundary } = input;
|
|
184
|
+
const facts = factsOf(input.swept.matrix.union);
|
|
185
|
+
const { declared, loaded } = boundary.consumers;
|
|
186
|
+
const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
|
|
187
|
+
const world = { closed, writeOnly: writeOnlyDeclarations(input) };
|
|
188
|
+
const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
189
|
+
const findings = [];
|
|
190
|
+
const pending = [];
|
|
191
|
+
facts.graph.symbols.forEach((symbol, at) => {
|
|
192
|
+
const candidate = candidates.get(symbol.id);
|
|
193
|
+
const found = candidate === undefined
|
|
194
|
+
? narrowing(facts, at, symbol, world)
|
|
195
|
+
: unreachable(facts, at, symbol, candidate, input);
|
|
196
|
+
if (found === undefined) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const named = boundary.edges.filter((side) => side.symbol === symbol.ref);
|
|
200
|
+
if (named.length === 0) {
|
|
201
|
+
findings.push(found);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const side of named) {
|
|
205
|
+
pending.push({ ...side, finding: found });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
pending.sort((a, b) => compare(a.edge, b.edge) || compare(a.side, b.side));
|
|
209
|
+
return { findings, pending };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The declarations the unreachable-export kind reports, whether or not a declared edge
|
|
213
|
+
* holds the finding pending: the unused-declaration kinds yield each to it.
|
|
214
|
+
*/
|
|
215
|
+
export function unreachableExports(input) {
|
|
216
|
+
const facts = factsOf(input.swept.matrix.union);
|
|
217
|
+
const found = new Set();
|
|
218
|
+
for (const candidate of input.swept.sweep.candidates) {
|
|
219
|
+
const at = facts.graph.at(candidate.id);
|
|
220
|
+
const symbol = facts.graph.symbols[at];
|
|
221
|
+
if (symbol !== undefined && unreachable(facts, at, symbol, candidate, input) !== undefined) {
|
|
222
|
+
found.add(symbol.id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return found;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The findings of the visibility-narrowing family, `DS1100` to `DS1199`. A finding a
|
|
229
|
+
* declared edge holds pending belongs in that edge's evaluation, which this analyzer
|
|
230
|
+
* does not publish yet, so a run that holds one is refused rather than reported
|
|
231
|
+
* without it.
|
|
232
|
+
*/
|
|
233
|
+
export const visibilityNarrowing = (input) => {
|
|
234
|
+
const { findings, pending } = narrowings(input);
|
|
235
|
+
if (pending.length > 0) {
|
|
236
|
+
throw new Error(`${String(pending.length)} finding(s) a declared edge holds pending, and no edge evaluation is published`);
|
|
237
|
+
}
|
|
238
|
+
return findings;
|
|
239
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which classes of the target implement which of its interfaces, and how each writes
|
|
3
|
+
* every method such an interface declares. A class implements an interface its
|
|
4
|
+
* `implements` clause names, in any instantiation, or one with a member its instance
|
|
5
|
+
* type is assignable to, asked only of a class carrying every member the interface
|
|
6
|
+
* requires and one it declares, and never of a generic side.
|
|
7
|
+
*/
|
|
8
|
+
import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
10
|
+
import { aliasChains } from "./alias-chain.js";
|
|
11
|
+
import { nodeKey } from "./inventory.js";
|
|
12
|
+
import { DEFAULT_BATCH_CAP } from "./references.js";
|
|
13
|
+
/** The interfaces, the classes and the `implements` entries of one project's own files. */
|
|
14
|
+
function walk(project, held) {
|
|
15
|
+
const interfaces = [];
|
|
16
|
+
const classes = [];
|
|
17
|
+
const clauses = [];
|
|
18
|
+
for (const file of project.ownSourceFiles()) {
|
|
19
|
+
const visit = (node) => {
|
|
20
|
+
if (isInterfaceDeclaration(node) || isClassDeclaration(node)) {
|
|
21
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
22
|
+
const name = node.name;
|
|
23
|
+
if (id !== undefined && name !== undefined) {
|
|
24
|
+
const declared = { id, nameNode: name, generic: node.typeParameters !== undefined };
|
|
25
|
+
if (isInterfaceDeclaration(node)) {
|
|
26
|
+
interfaces.push(declared);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
classes.push(declared);
|
|
30
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
31
|
+
if (isHeritageClause(clause) && clause.token === SyntaxKind.ImplementsKeyword) {
|
|
32
|
+
clauses.push(...clause.types.map((entry) => ({ owner: declared, entry })));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
node.forEachChild(visit);
|
|
39
|
+
};
|
|
40
|
+
file.forEachChild(visit);
|
|
41
|
+
}
|
|
42
|
+
return { interfaces, classes, clauses };
|
|
43
|
+
}
|
|
44
|
+
/** The type at each node, asked in capped batches. */
|
|
45
|
+
function typesAt(project, nodes) {
|
|
46
|
+
const typeAt = new Map();
|
|
47
|
+
for (let from = 0; from < nodes.length; from += DEFAULT_BATCH_CAP) {
|
|
48
|
+
const run = nodes.slice(from, from + DEFAULT_BATCH_CAP);
|
|
49
|
+
project.checker.getTypeAtLocation(run).forEach((type, index) => {
|
|
50
|
+
const node = run[index];
|
|
51
|
+
if (type !== undefined && node !== undefined) {
|
|
52
|
+
typeAt.set(node, type);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return typeAt;
|
|
57
|
+
}
|
|
58
|
+
/** Whether one class member is a method whose every written body holds no statement. */
|
|
59
|
+
function emptyImplementation(project, member) {
|
|
60
|
+
const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
|
|
61
|
+
let bodies = 0;
|
|
62
|
+
for (const handle of member.declarations) {
|
|
63
|
+
// A body outside the target is one this analysis cannot read, so it is not empty.
|
|
64
|
+
if (!own.has(handle.path)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const node = project.declarationAt(handle)?.node;
|
|
68
|
+
if (node === undefined || !isMethodDeclaration(node)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (node.body !== undefined) {
|
|
72
|
+
if (node.body.statements.length > 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
bodies += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return bodies > 0;
|
|
79
|
+
}
|
|
80
|
+
/** How one project's interfaces are implemented by its classes. */
|
|
81
|
+
export function implementations(project, held) {
|
|
82
|
+
const classes = new Map();
|
|
83
|
+
const bodies = new Map();
|
|
84
|
+
const walked = walk(project, held);
|
|
85
|
+
if (walked.interfaces.length === 0 || walked.classes.length === 0) {
|
|
86
|
+
return { classes, bodies };
|
|
87
|
+
}
|
|
88
|
+
const checker = project.checker;
|
|
89
|
+
const typeAt = typesAt(project, [
|
|
90
|
+
...walked.interfaces.map((one) => one.nameNode),
|
|
91
|
+
...walked.classes.map((one) => one.nameNode),
|
|
92
|
+
...walked.clauses.map((clause) => clause.entry),
|
|
93
|
+
]);
|
|
94
|
+
// Two declarations of one interface merge into one type.
|
|
95
|
+
const byType = new Map();
|
|
96
|
+
for (const declared of walked.interfaces) {
|
|
97
|
+
const type = typeAt.get(declared.nameNode);
|
|
98
|
+
if (type === undefined) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const merged = byType.get(type.id);
|
|
102
|
+
if (merged === undefined) {
|
|
103
|
+
byType.set(type.id, { ids: [declared.id], type, generic: declared.generic });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
merged.ids.push(declared.id);
|
|
107
|
+
merged.generic ||= declared.generic;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const classAt = new Map();
|
|
111
|
+
for (const owner of walked.classes) {
|
|
112
|
+
const type = typeAt.get(owner.nameNode);
|
|
113
|
+
if (type !== undefined) {
|
|
114
|
+
classAt.set(owner.id, { owner, type });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const membersOf = (at) => (at.members ??= checker.getPropertiesOfType(at.type));
|
|
118
|
+
const classMembers = (at) => (at.members ??= new Map(checker.getPropertiesOfType(at.type).map((member) => [member.escapedName, member])));
|
|
119
|
+
const methodIds = new Set(held.symbols.filter((symbol) => symbol.kind === "interface-method").map((symbol) => symbol.id));
|
|
120
|
+
const chains = aliasChains(project, held);
|
|
121
|
+
const record = (at, implementer) => {
|
|
122
|
+
for (const id of at.ids) {
|
|
123
|
+
let set = classes.get(id);
|
|
124
|
+
if (set === undefined) {
|
|
125
|
+
set = new Set();
|
|
126
|
+
classes.set(id, set);
|
|
127
|
+
}
|
|
128
|
+
set.add(implementer.owner.id);
|
|
129
|
+
}
|
|
130
|
+
const written = classMembers(implementer);
|
|
131
|
+
for (const member of membersOf(at)) {
|
|
132
|
+
const answering = written.get(member.escapedName);
|
|
133
|
+
if (answering === undefined) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const empty = emptyImplementation(project, answering);
|
|
137
|
+
for (const id of chains.declarationsOf(member)) {
|
|
138
|
+
if (!methodIds.has(id)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let byClass = bodies.get(id);
|
|
142
|
+
if (byClass === undefined) {
|
|
143
|
+
byClass = new Map();
|
|
144
|
+
bodies.set(id, byClass);
|
|
145
|
+
}
|
|
146
|
+
byClass.set(implementer.owner.id, empty);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const recorded = new Set();
|
|
151
|
+
const pair = (at, implementer) => {
|
|
152
|
+
const key = `${String(at.type.id)}:${implementer.owner.id}`;
|
|
153
|
+
if (!recorded.has(key)) {
|
|
154
|
+
recorded.add(key);
|
|
155
|
+
record(at, implementer);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
for (const clause of walked.clauses) {
|
|
159
|
+
const entry = typeAt.get(clause.entry);
|
|
160
|
+
const implementer = classAt.get(clause.owner.id);
|
|
161
|
+
if (entry === undefined || implementer === undefined) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const at = byType.get(entry.isTypeReference() ? entry.getTarget().id : entry.id);
|
|
165
|
+
if (at !== undefined) {
|
|
166
|
+
pair(at, implementer);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const at of byType.values()) {
|
|
170
|
+
const members = at.generic ? [] : membersOf(at);
|
|
171
|
+
if (members.length === 0) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const required = members.filter((member) => (member.flags & SymbolFlags.Optional) === 0);
|
|
175
|
+
for (const implementer of classAt.values()) {
|
|
176
|
+
if (implementer.owner.generic) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const written = classMembers(implementer);
|
|
180
|
+
if (required.every((member) => written.has(member.escapedName)) &&
|
|
181
|
+
members.some((member) => written.has(member.escapedName)) &&
|
|
182
|
+
checker.isTypeAssignableTo(implementer.type, at.type)) {
|
|
183
|
+
pair(at, implementer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { classes, bodies };
|
|
188
|
+
}
|
|
189
|
+
/** Every project's answers as one run's, each implementation once. */
|
|
190
|
+
export function mergeImplementations(per) {
|
|
191
|
+
const classes = new Map();
|
|
192
|
+
const bodies = new Map();
|
|
193
|
+
for (const one of per) {
|
|
194
|
+
for (const [id, held] of one.classes) {
|
|
195
|
+
const set = classes.get(id) ?? new Set();
|
|
196
|
+
classes.set(id, set);
|
|
197
|
+
for (const owner of held) {
|
|
198
|
+
set.add(owner);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [id, held] of one.bodies) {
|
|
202
|
+
const byClass = bodies.get(id) ?? new Map();
|
|
203
|
+
bodies.set(id, byClass);
|
|
204
|
+
for (const [owner, empty] of held) {
|
|
205
|
+
byClass.set(owner, (byClass.get(owner) ?? true) && empty);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { classes, bodies };
|
|
210
|
+
}
|
package/dist/src/inventory.js
CHANGED
|
@@ -13,7 +13,7 @@ const MANIFEST = "package.json";
|
|
|
13
13
|
* One directory is read at most once, because a package holds many files and a
|
|
14
14
|
* manifest read is a filesystem call.
|
|
15
15
|
*/
|
|
16
|
-
function
|
|
16
|
+
export function packageScope(host, root) {
|
|
17
17
|
const named = new Map();
|
|
18
18
|
const manifestName = (dir) => {
|
|
19
19
|
let text;
|
|
@@ -233,7 +233,7 @@ function typeMemberKind(node, inInterface) {
|
|
|
233
233
|
export function inventory(project, host, targetRoot) {
|
|
234
234
|
const files = project.ownSourceFiles();
|
|
235
235
|
const ownFiles = new Set(files.map((file) => file.fileName));
|
|
236
|
-
const moduleOf =
|
|
236
|
+
const moduleOf = packageScope(host, targetRoot);
|
|
237
237
|
const building = [];
|
|
238
238
|
const byNode = new Map();
|
|
239
239
|
const exportScopes = [];
|
package/dist/src/resolve.js
CHANGED
|
@@ -4,7 +4,7 @@ import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } f
|
|
|
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;
|
|
@@ -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/jsr.json
CHANGED