@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.18
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 +26 -71
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +2 -2
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +41 -107
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +2 -2
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/published-reach.ts +9 -4
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
package/src/sweep.ts
DELETED
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which declarations of one graph are dead, and under which liveness relation.
|
|
3
|
-
* Reference counting holds a declaration live when any declaration references it,
|
|
4
|
-
* whatever that one's own liveness, or when it is a root of a kind that names a caller;
|
|
5
|
-
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
|
-
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
|
-
import { namesACaller, OUTSIDE, type Counts, type Edge, type Graph } from "./graph.ts";
|
|
11
|
-
import { edgeWithheldBy } from "./tests/file-class.ts";
|
|
12
|
-
import type { InventorySymbol } from "./inventory.ts";
|
|
13
|
-
import type { Position } from "./position.ts";
|
|
14
|
-
|
|
15
|
-
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
16
|
-
export type Relation = "reference-counting" | "reachability";
|
|
17
|
-
|
|
18
|
-
/** One declaration an exemption class holds back, and the evidence it was found by. */
|
|
19
|
-
export interface Exemption {
|
|
20
|
-
/** The identifier of the declaration held back. */
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly class: TSExemptionClass;
|
|
23
|
-
/** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
|
|
24
|
-
readonly detail: string;
|
|
25
|
-
/** Where the evidence is written. */
|
|
26
|
-
readonly site: Position;
|
|
27
|
-
/**
|
|
28
|
-
* The component file, below the target root, whose markup holds the evidence. The
|
|
29
|
-
* record holds only while that file is live.
|
|
30
|
-
*/
|
|
31
|
-
readonly whileLive?: string;
|
|
32
|
-
/**
|
|
33
|
-
* The interface methods the retained member implements, and whether its body is empty.
|
|
34
|
-
* The record holds only while one of them is called through its interface.
|
|
35
|
-
*/
|
|
36
|
-
readonly implementing?: Implementing;
|
|
37
|
-
/** Whether the record holds only where a write the mode counts stores into the declaration. */
|
|
38
|
-
readonly whileWritten?: true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** The interface methods one retained class member implements, and whether its body is empty. */
|
|
42
|
-
export interface Implementing {
|
|
43
|
-
/** The declarations of the interface's methods the member answers, by identifier. */
|
|
44
|
-
readonly methods: readonly string[];
|
|
45
|
-
readonly empty: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The run's reference mode, decided once by the composition and read by every stage
|
|
50
|
-
* that counts references.
|
|
51
|
-
*/
|
|
52
|
-
export interface Mode {
|
|
53
|
-
/**
|
|
54
|
-
* Counts no reference a test file made, under either relation. Every reference a root
|
|
55
|
-
* in a test file makes is such a reference, so nothing only a test reaches is live.
|
|
56
|
-
*/
|
|
57
|
-
readonly production: boolean;
|
|
58
|
-
/**
|
|
59
|
-
* Classes a reference a loaded consumer's test file made as a production one rather than a
|
|
60
|
-
* test one, `analysis.consumer_tests` at `production`: a classification, not a filter, so it
|
|
61
|
-
* decides the split a candidate reports as well as what a production sweep counts.
|
|
62
|
-
*/
|
|
63
|
-
readonly consumerTestsProduction?: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The references to one declaration the mode counts: under `production` none test code made but those it classes as production. */
|
|
67
|
-
function countedRefs(made: Counts | undefined, mode: Mode): number {
|
|
68
|
-
if (made === undefined) {
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
|
-
if (!mode.production) {
|
|
72
|
-
return made.production + made.test;
|
|
73
|
-
}
|
|
74
|
-
return made.production + (mode.consumerTestsProduction === true ? made.consumerTest : 0);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The references to one declaration, split as the mode classes them, whatever it counts. */
|
|
78
|
-
export function splitRefs(
|
|
79
|
-
made: Counts | undefined,
|
|
80
|
-
mode: Mode,
|
|
81
|
-
): { readonly production: number; readonly test: number } {
|
|
82
|
-
const moved = mode.consumerTestsProduction === true ? (made?.consumerTest ?? 0) : 0;
|
|
83
|
-
return { production: (made?.production ?? 0) + moved, test: (made?.test ?? 0) - moved };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** What one sweep runs over besides the graph. */
|
|
87
|
-
export interface SweepInput {
|
|
88
|
-
/**
|
|
89
|
-
* The declarations a matched suppression names. A mark holds its declaration live
|
|
90
|
-
* under both relations and seeds reachability through every reference it makes, so
|
|
91
|
-
* nothing only a marked declaration references is dead.
|
|
92
|
-
*/
|
|
93
|
-
readonly marked: readonly string[];
|
|
94
|
-
/**
|
|
95
|
-
* The exemptions the run computed. An exempt declaration is never a candidate, and it
|
|
96
|
-
* seeds reachability as a mark does, so nothing it references is dead; it is live
|
|
97
|
-
* under reference counting only where something references it. Absent, the sweep
|
|
98
|
-
* holds nothing back.
|
|
99
|
-
*/
|
|
100
|
-
readonly exempt?: readonly Exemption[];
|
|
101
|
-
/**
|
|
102
|
-
* The declarations a question the checker did not answer could have kept live. Each
|
|
103
|
-
* is held as an exempt declaration is. Absent, the sweep holds nothing back for one.
|
|
104
|
-
*/
|
|
105
|
-
readonly unanswered?: readonly string[];
|
|
106
|
-
readonly mode: Mode;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** One dead declaration, the relation that found it, and the counts a kind reads. */
|
|
110
|
-
export interface Candidate {
|
|
111
|
-
readonly id: string;
|
|
112
|
-
/** The references a production file made to the declaration, whatever the mode counts. */
|
|
113
|
-
readonly productionRefs: number;
|
|
114
|
-
/** The references a test file made to it. */
|
|
115
|
-
readonly testRefs: number;
|
|
116
|
-
/**
|
|
117
|
-
* Reference counting where the mode counts no reference to the declaration, and
|
|
118
|
-
* reachability otherwise, so a declaration dead under both carries the stronger claim.
|
|
119
|
-
*/
|
|
120
|
-
readonly relation: Relation;
|
|
121
|
-
/**
|
|
122
|
-
* Whether the test-of-dead-code rule admitted the declaration: a test declaration whose
|
|
123
|
-
* set of referenced production declarations is non-empty and wholly dead.
|
|
124
|
-
*/
|
|
125
|
-
readonly testOfDeadCode: boolean;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** One graph's answer: what each relation holds live, and what is dead. */
|
|
129
|
-
export interface Liveness {
|
|
130
|
-
/**
|
|
131
|
-
* Per declaration, the relations that hold it live, reference counting first. A
|
|
132
|
-
* declaration neither holds live has no entry.
|
|
133
|
-
*/
|
|
134
|
-
readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
|
|
135
|
-
/** The dead declarations, in the order the inventory holds them. */
|
|
136
|
-
readonly candidates: readonly Candidate[];
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Answers which declarations of one graph are dead under the input's mode.
|
|
141
|
-
*
|
|
142
|
-
* The order is the order the answers depend on: the callers and the marks are live
|
|
143
|
-
* before either relation runs, the candidate set is every judged declaration at least
|
|
144
|
-
* one relation does not hold live and no exemption holds back, and the tests of dead
|
|
145
|
-
* code join it.
|
|
146
|
-
*/
|
|
147
|
-
export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
148
|
-
const marked = flagged(graph, input.marked);
|
|
149
|
-
const exempt = flagged(graph, [
|
|
150
|
-
...(input.exempt ?? []).map((record) => record.id),
|
|
151
|
-
...(input.unanswered ?? []),
|
|
152
|
-
]);
|
|
153
|
-
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
154
|
-
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
155
|
-
// names a caller, and a mark, are live under reference counting as well; a root that
|
|
156
|
-
// only supposes a caller is not, and neither is an exemption.
|
|
157
|
-
const called = graph.symbols.map(() => false);
|
|
158
|
-
for (const root of graph.rooted) {
|
|
159
|
-
if (namesACaller(root.kind)) {
|
|
160
|
-
called[root.at] = true;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const counted = (at: number): number => countedRefs(graph.made[at], input.mode);
|
|
165
|
-
const reached = reachable(graph, held, input.mode);
|
|
166
|
-
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
167
|
-
const relations: Relation[] = [];
|
|
168
|
-
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
169
|
-
relations.push("reference-counting");
|
|
170
|
-
}
|
|
171
|
-
if (reached[at] === true) {
|
|
172
|
-
relations.push("reachability");
|
|
173
|
-
}
|
|
174
|
-
return relations;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const dead = graph.symbols.map(
|
|
178
|
-
(_symbol, at) =>
|
|
179
|
-
graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2,
|
|
180
|
-
);
|
|
181
|
-
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, held);
|
|
182
|
-
|
|
183
|
-
const candidates: Candidate[] = [];
|
|
184
|
-
const live = new Map<string, readonly Relation[]>();
|
|
185
|
-
graph.symbols.forEach((symbol, at) => {
|
|
186
|
-
const relations = liveUnder[at] ?? [];
|
|
187
|
-
if (relations.length > 0) {
|
|
188
|
-
live.set(symbol.id, relations);
|
|
189
|
-
}
|
|
190
|
-
if (dead[at] !== true) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const split = splitRefs(graph.made[at], input.mode);
|
|
194
|
-
candidates.push({
|
|
195
|
-
id: symbol.id,
|
|
196
|
-
productionRefs: split.production,
|
|
197
|
-
testRefs: split.test,
|
|
198
|
-
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
199
|
-
testOfDeadCode: testOfDeadCode[at] === true,
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return { liveUnder: live, candidates };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* The directory a test runner reads a manual mock from, whose exports it puts in place of
|
|
208
|
-
* the module's: https://jestjs.io/docs/manual-mocks
|
|
209
|
-
*/
|
|
210
|
-
const MANUAL_MOCKS = "__mocks__";
|
|
211
|
-
|
|
212
|
-
/** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
|
|
213
|
-
export function runnerCalls(symbol: InventorySymbol): boolean {
|
|
214
|
-
return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Per declaration of the graph, whether the run that counts test references reaches it.
|
|
219
|
-
* That run's roots are the graph's own and, of each test file, its top level and every
|
|
220
|
-
* function it exports, which a test runner may call, and every export of a manual mock,
|
|
221
|
-
* which the runner reads; any other export of a test file, and test-support code, which
|
|
222
|
-
* runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
|
|
223
|
-
* seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
|
|
224
|
-
*/
|
|
225
|
-
export function reachedWithTheTests(
|
|
226
|
-
graph: Graph,
|
|
227
|
-
held: readonly string[],
|
|
228
|
-
namespaces = true,
|
|
229
|
-
): readonly boolean[] {
|
|
230
|
-
const tests = graph.symbols.flatMap((symbol, at) => {
|
|
231
|
-
const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
|
|
232
|
-
const called = exported && runnerCalls(symbol);
|
|
233
|
-
return graph.test[at] === true &&
|
|
234
|
-
graph.support[at] !== true &&
|
|
235
|
-
(symbol.kind === "file" || called)
|
|
236
|
-
? [{ at, kind: "test-runner" as const }]
|
|
237
|
-
: [];
|
|
238
|
-
});
|
|
239
|
-
return reachable(
|
|
240
|
-
{ ...graph, rooted: [...graph.rooted, ...tests] },
|
|
241
|
-
flagged(graph, held),
|
|
242
|
-
{ production: false },
|
|
243
|
-
namespaces,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The references a loaded consumer or an unheld file makes that the mode counts. A
|
|
249
|
-
* production mode counts no test reference, but a loaded consumer's where the mode classes
|
|
250
|
-
* a consumer's tests as production.
|
|
251
|
-
*/
|
|
252
|
-
function callsOf(graph: Graph, mode: Mode): readonly Edge[] {
|
|
253
|
-
return graph.consumed.filter((edge) => !edgeWithheldBy(mode, edge));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
257
|
-
function flagged(graph: Graph, ids: readonly string[]): boolean[] {
|
|
258
|
-
const flags = graph.symbols.map(() => false);
|
|
259
|
-
for (const id of ids) {
|
|
260
|
-
const at = graph.at(id);
|
|
261
|
-
if (at !== OUTSIDE) {
|
|
262
|
-
flags[at] = true;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return flags;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
270
|
-
* reference sets: a held declaration, marked or exempt, is live by a mechanism the
|
|
271
|
-
* analysis cannot see, so no mode withholds a reference it makes, a test declaration's
|
|
272
|
-
* included, while a root reaches through the references the mode counts. The held seed
|
|
273
|
-
* runs first, so what it reaches is expanded under the wider rule.
|
|
274
|
-
*/
|
|
275
|
-
function reachable(
|
|
276
|
-
graph: Graph,
|
|
277
|
-
held: readonly boolean[],
|
|
278
|
-
mode: Mode,
|
|
279
|
-
namespaces = true,
|
|
280
|
-
): readonly boolean[] {
|
|
281
|
-
const reached = graph.symbols.map(() => false);
|
|
282
|
-
const expanded = graph.symbols.map(() => false);
|
|
283
|
-
const queue: number[] = [];
|
|
284
|
-
|
|
285
|
-
// Reaching a file reaches its top level: the references written outside every
|
|
286
|
-
// declaration it holds. A reference that reads a file reads the module's namespace,
|
|
287
|
-
// which reaches what the module exports as well. An evaluation runs the top level
|
|
288
|
-
// and reads no export, and a root does not expand either: a file a runtime runs
|
|
289
|
-
// without reading its exports is a root by itself.
|
|
290
|
-
const enter = (at: number, expands: boolean): void => {
|
|
291
|
-
if (!reached[at]) {
|
|
292
|
-
reached[at] = true;
|
|
293
|
-
queue.push(at);
|
|
294
|
-
}
|
|
295
|
-
if (!expands || expanded[at] === true) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
expanded[at] = true;
|
|
299
|
-
for (const exported of graph.exportsOf[at] ?? []) {
|
|
300
|
-
enter(exported, false);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
const walk = (holding: boolean): void => {
|
|
304
|
-
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
305
|
-
for (const edge of graph.out[at] ?? []) {
|
|
306
|
-
if (!holding && edgeWithheldBy(mode, edge)) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
enter(edge.to, namespaces && !edge.evaluation);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
held.forEach((hold, at) => {
|
|
315
|
-
if (hold) {
|
|
316
|
-
enter(at, false);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
walk(true);
|
|
320
|
-
for (const root of graph.rooted) {
|
|
321
|
-
enter(root.at, false);
|
|
322
|
-
}
|
|
323
|
-
// A consumer's reference reaches what it names as one of the target's own does.
|
|
324
|
-
for (const edge of callsOf(graph, mode)) {
|
|
325
|
-
enter(edge.to, namespaces && !edge.evaluation);
|
|
326
|
-
}
|
|
327
|
-
walk(false);
|
|
328
|
-
return reached;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Admits every test declaration whose set of referenced production declarations is
|
|
333
|
-
* non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
|
|
334
|
-
* code the target does not implement, so it is no production declaration here. The set is
|
|
335
|
-
* read from every reference the test makes, which a production sweep does not count. A
|
|
336
|
-
* test-support declaration a test referencing a live target also references is a live
|
|
337
|
-
* target of every test referencing it. A marked or exempt test is never admitted.
|
|
338
|
-
*/
|
|
339
|
-
function admitTestsOfDeadCode(
|
|
340
|
-
graph: Graph,
|
|
341
|
-
dead: boolean[],
|
|
342
|
-
held: readonly boolean[],
|
|
343
|
-
): readonly boolean[] {
|
|
344
|
-
const testCode = (at: number): boolean => graph.test[at] === true || graph.support[at] === true;
|
|
345
|
-
const judged = graph.symbols.map((_symbol, at) => {
|
|
346
|
-
let targets = 0;
|
|
347
|
-
let live = 0;
|
|
348
|
-
if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
|
|
349
|
-
for (const edge of graph.out[at] ?? []) {
|
|
350
|
-
if (
|
|
351
|
-
testCode(edge.to) ||
|
|
352
|
-
graph.subject[edge.to] !== true ||
|
|
353
|
-
graph.ambient[edge.to] === true
|
|
354
|
-
) {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
targets += 1;
|
|
358
|
-
if (dead[edge.to] !== true) {
|
|
359
|
-
live += 1;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return { targets, live };
|
|
364
|
-
});
|
|
365
|
-
const shared = new Set<number>();
|
|
366
|
-
judged.forEach(({ live }, at) => {
|
|
367
|
-
if (live > 0) {
|
|
368
|
-
for (const edge of graph.out[at] ?? []) {
|
|
369
|
-
if (graph.support[edge.to] === true) {
|
|
370
|
-
shared.add(edge.to);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
const admitted = graph.symbols.map(() => false);
|
|
376
|
-
judged.forEach(({ targets, live }, at) => {
|
|
377
|
-
const sharing = (graph.out[at] ?? []).some((edge) => shared.has(edge.to) && edge.to !== at);
|
|
378
|
-
if (targets > 0 && live === 0 && !sharing) {
|
|
379
|
-
dead[at] = true;
|
|
380
|
-
admitted[at] = true;
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
return admitted;
|
|
384
|
-
}
|