@cplieger/deadset-ts 8.0.0-dev.5 → 8.0.0-dev.6
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 +3 -3
- package/dist/src/config.js +1 -1
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +1071 -0
- package/dist/src/contract/report.generated.js +796 -0
- package/dist/src/exempt-classes.js +1 -59
- package/dist/src/exempt.js +8 -19
- package/dist/src/findings/completion.js +11 -20
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +4 -4
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +8 -7
- package/dist/src/findings/non-code-artifacts.js +5 -6
- package/dist/src/findings/reads-and-writes.js +9 -8
- package/dist/src/findings/self-check.js +7 -10
- package/dist/src/findings/unused-declarations.js +28 -27
- package/dist/src/findings/visibility-narrowing.js +9 -11
- package/dist/src/findings-pass.js +1 -3
- package/dist/src/index.js +1 -1
- package/dist/src/intra-function-parts.js +8 -13
- package/dist/src/kinds.js +4 -360
- package/dist/src/report-schema.js +273 -0
- package/dist/src/report.js +17 -11
- package/dist/src/resolve.js +1 -6
- package/dist/src/run.js +2 -1
- package/dist/src/setup-failure.js +2 -6
- package/dist/src/suppress-file.js +4 -3
- package/dist/src/suppress-inline.js +3 -2
- package/dist/src/suppress.js +29 -48
- package/dist/src/verbs/analyze.js +7 -2
- package/dist/src/verbs/describe.js +4 -4
- package/dist/src/verbs/explain.js +2 -1
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +2 -1
- package/dist/src/verbs/print-retained.js +2 -1
- package/dist/src/verbs/print-roots.js +2 -1
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +2 -2
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +3 -3
- package/src/config.ts +7 -13
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +1123 -0
- package/src/contract/report.generated.ts +1157 -0
- package/src/exempt-classes.ts +1 -70
- package/src/exempt.ts +12 -20
- package/src/finding.ts +9 -9
- package/src/findings/completion.ts +22 -23
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +6 -7
- package/src/findings/emitters.ts +6 -6
- package/src/findings/interfaces.ts +4 -6
- package/src/findings/intra-function.ts +8 -8
- package/src/findings/non-code-artifacts.ts +5 -7
- package/src/findings/reads-and-writes.ts +13 -11
- package/src/findings/self-check.ts +8 -15
- package/src/findings/unused-declarations.ts +28 -27
- package/src/findings/visibility-narrowing.ts +9 -12
- package/src/findings-pass.ts +1 -4
- package/src/index.ts +1 -1
- package/src/intra-function-parts.ts +8 -14
- package/src/kinds.ts +6 -406
- package/src/report-schema.ts +366 -0
- package/src/report.ts +42 -195
- package/src/reporters/annotations.ts +2 -2
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -2
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +13 -8
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +11 -11
- package/src/run.ts +2 -1
- package/src/setup-failure.ts +5 -7
- package/src/suppress-file.ts +3 -4
- package/src/suppress-inline.ts +2 -2
- package/src/suppress.ts +39 -55
- package/src/verbs/analyze.ts +16 -4
- package/src/verbs/describe.ts +10 -4
- package/src/verbs/explain.ts +2 -1
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +2 -1
- package/src/verbs/print-retained.ts +2 -1
- package/src/verbs/print-roots.ts +2 -1
- package/src/verbs/verb.ts +0 -6
- package/src/verbs/version.ts +2 -2
- package/dist/src/version.js +0 -7
- package/src/version.ts +0 -7
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds a report to `contract/report.schema.json` and `contract/finding.schema.json`
|
|
3
|
+
* before it is written, reading the schemas the generator copies from the Contract, so
|
|
4
|
+
* this analyzer never writes a document a reader refuses. It evaluates the keywords the
|
|
5
|
+
* Contract's schemas use; the schemas' patterns are written to match alike in RE2 and in
|
|
6
|
+
* ECMAScript.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { REPORT_SCHEMAS, type Report } from "./contract/report.generated.ts";
|
|
10
|
+
|
|
11
|
+
/** One value a schema refuses: its JSON Pointer, the keyword, and why. */
|
|
12
|
+
interface SchemaRefusal {
|
|
13
|
+
readonly pointer: string;
|
|
14
|
+
readonly keyword: string;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Schema = Readonly<Record<string, unknown>>;
|
|
19
|
+
type Documents = Readonly<Record<string, Schema>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Every keyword the validator evaluates or knows to carry no assertion. A schema naming
|
|
23
|
+
* another fails every check rather than passing values the keyword would refuse.
|
|
24
|
+
*/
|
|
25
|
+
const KEYWORDS: ReadonlySet<string> = new Set([
|
|
26
|
+
...["$schema", "$id", "$comment", "$defs", "title", "description", "default", "examples"],
|
|
27
|
+
...["$ref", "type", "enum", "const", "pattern", "minLength", "minimum", "maximum"],
|
|
28
|
+
...["properties", "additionalProperties", "required", "minProperties"],
|
|
29
|
+
...["items", "minItems", "maxItems", "uniqueItems"],
|
|
30
|
+
...["allOf", "anyOf", "oneOf", "not", "if", "then", "else"],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const patterns = new Map<string, RegExp>();
|
|
34
|
+
|
|
35
|
+
function pattern(source: string): RegExp {
|
|
36
|
+
let held = patterns.get(source);
|
|
37
|
+
if (held === undefined) {
|
|
38
|
+
held = new RegExp(source, "u");
|
|
39
|
+
patterns.set(source, held);
|
|
40
|
+
}
|
|
41
|
+
return held;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
45
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function typeOf(value: unknown): string {
|
|
49
|
+
if (value === null) {
|
|
50
|
+
return "null";
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return "array";
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "number") {
|
|
56
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
57
|
+
}
|
|
58
|
+
return typeof value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasType(value: unknown, name: string): boolean {
|
|
62
|
+
const held = typeOf(value);
|
|
63
|
+
return held === name || (name === "number" && held === "integer");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One value's encoding with object members sorted, so two equal values encode alike. */
|
|
67
|
+
function canonical(value: unknown): string {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
70
|
+
}
|
|
71
|
+
if (isObject(value)) {
|
|
72
|
+
return `{${Object.keys(value)
|
|
73
|
+
.sort()
|
|
74
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
75
|
+
.join(",")}}`;
|
|
76
|
+
}
|
|
77
|
+
return JSON.stringify(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A string's length in code points, which JSON Schema's minLength counts. */
|
|
81
|
+
function codePoints(value: string): number {
|
|
82
|
+
let counted = 0;
|
|
83
|
+
for (const _point of value) {
|
|
84
|
+
counted += 1;
|
|
85
|
+
}
|
|
86
|
+
return counted;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const escape = (name: string): string => name.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
90
|
+
|
|
91
|
+
/** A schema evaluated in the document it is read from, so a relative `$ref` resolves there. */
|
|
92
|
+
class Checker {
|
|
93
|
+
private readonly documents: Documents;
|
|
94
|
+
|
|
95
|
+
constructor(documents: Documents) {
|
|
96
|
+
this.documents = documents;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolve(doc: string, ref: string): readonly [string, Schema] {
|
|
100
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
101
|
+
const target = file === "" ? doc : file;
|
|
102
|
+
let schema: unknown = this.documents[target];
|
|
103
|
+
for (const token of pointer.split("/").filter((one) => one !== "")) {
|
|
104
|
+
schema = isObject(schema)
|
|
105
|
+
? schema[token.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!isObject(schema)) {
|
|
109
|
+
throw new Error(`report schema: ${ref} names no schema`);
|
|
110
|
+
}
|
|
111
|
+
return [target, schema];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
matches(doc: string, schema: Schema, value: unknown, at: string): boolean {
|
|
115
|
+
const found: SchemaRefusal[] = [];
|
|
116
|
+
this.check(doc, schema, value, at, found);
|
|
117
|
+
return found.length === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
check(doc: string, schema: Schema, value: unknown, at: string, found: SchemaRefusal[]): void {
|
|
121
|
+
for (const keyword of Object.keys(schema)) {
|
|
122
|
+
if (!KEYWORDS.has(keyword) && !keyword.startsWith("x-")) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`report schema: the keyword ${keyword} is not one this validator evaluates`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const refuse = (keyword: string, reason: string, pointer = at): void => {
|
|
129
|
+
found.push({ pointer, keyword, reason });
|
|
130
|
+
};
|
|
131
|
+
if (typeof schema["$ref"] === "string") {
|
|
132
|
+
const [target, referenced] = this.resolve(doc, schema["$ref"]);
|
|
133
|
+
this.check(target, referenced, value, at, found);
|
|
134
|
+
}
|
|
135
|
+
const types = schema["type"];
|
|
136
|
+
const named = types === undefined ? [] : Array.isArray(types) ? types : [types];
|
|
137
|
+
if (named.length > 0 && !named.some((one) => hasType(value, String(one)))) {
|
|
138
|
+
refuse("type", `the value is ${typeOf(value)}, want ${named.join(", ")}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.checkValue(schema, value, refuse);
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
this.checkArray(doc, schema, value, at, found, refuse);
|
|
144
|
+
} else if (isObject(value)) {
|
|
145
|
+
this.checkObject(doc, schema, value, at, found, refuse);
|
|
146
|
+
}
|
|
147
|
+
this.checkApplicators(doc, schema, value, at, found, refuse);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private checkValue(
|
|
151
|
+
schema: Schema,
|
|
152
|
+
value: unknown,
|
|
153
|
+
refuse: (keyword: string, reason: string) => void,
|
|
154
|
+
): void {
|
|
155
|
+
const values = schema["enum"];
|
|
156
|
+
if (Array.isArray(values) && !values.some((one) => canonical(one) === canonical(value))) {
|
|
157
|
+
refuse("enum", `${canonical(value)} is not one of the values the schema lists`);
|
|
158
|
+
}
|
|
159
|
+
if (schema["const"] !== undefined && canonical(schema["const"]) !== canonical(value)) {
|
|
160
|
+
refuse("const", `${canonical(value)} is not ${canonical(schema["const"])}`);
|
|
161
|
+
}
|
|
162
|
+
if (typeof value === "string") {
|
|
163
|
+
if (typeof schema["pattern"] === "string" && !pattern(schema["pattern"]).test(value)) {
|
|
164
|
+
refuse("pattern", `${JSON.stringify(value)} does not match ${schema["pattern"]}`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof schema["minLength"] === "number" && codePoints(value) < schema["minLength"]) {
|
|
167
|
+
refuse(
|
|
168
|
+
"minLength",
|
|
169
|
+
`${JSON.stringify(value)} is shorter than ${String(schema["minLength"])} characters`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "number") {
|
|
174
|
+
if (typeof schema["minimum"] === "number" && value < schema["minimum"]) {
|
|
175
|
+
refuse("minimum", `${String(value)} is below ${String(schema["minimum"])}`);
|
|
176
|
+
}
|
|
177
|
+
if (typeof schema["maximum"] === "number" && value > schema["maximum"]) {
|
|
178
|
+
refuse("maximum", `${String(value)} is above ${String(schema["maximum"])}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private checkObject(
|
|
184
|
+
doc: string,
|
|
185
|
+
schema: Schema,
|
|
186
|
+
value: Readonly<Record<string, unknown>>,
|
|
187
|
+
at: string,
|
|
188
|
+
found: SchemaRefusal[],
|
|
189
|
+
refuse: (keyword: string, reason: string) => void,
|
|
190
|
+
): void {
|
|
191
|
+
const properties = isObject(schema["properties"]) ? schema["properties"] : {};
|
|
192
|
+
const additional = schema["additionalProperties"];
|
|
193
|
+
for (const name of Object.keys(value).sort()) {
|
|
194
|
+
const member = `${at}/${escape(name)}`;
|
|
195
|
+
const declared = properties[name];
|
|
196
|
+
if (isObject(declared)) {
|
|
197
|
+
this.check(doc, declared, value[name], member, found);
|
|
198
|
+
} else if (additional === false) {
|
|
199
|
+
refuse(
|
|
200
|
+
"additionalProperties",
|
|
201
|
+
`the member ${JSON.stringify(name)} is not one the schema declares`,
|
|
202
|
+
);
|
|
203
|
+
} else if (isObject(additional)) {
|
|
204
|
+
this.check(doc, additional, value[name], member, found);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const name of Array.isArray(schema["required"]) ? (schema["required"] as string[]) : []) {
|
|
208
|
+
if (!Object.hasOwn(value, name)) {
|
|
209
|
+
refuse("required", `the member ${JSON.stringify(name)} is absent`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const fewest = schema["minProperties"];
|
|
213
|
+
if (typeof fewest === "number" && Object.keys(value).length < fewest) {
|
|
214
|
+
refuse(
|
|
215
|
+
"minProperties",
|
|
216
|
+
`the object holds ${String(Object.keys(value).length)} members, fewer than ${String(fewest)}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private checkArray(
|
|
222
|
+
doc: string,
|
|
223
|
+
schema: Schema,
|
|
224
|
+
value: readonly unknown[],
|
|
225
|
+
at: string,
|
|
226
|
+
found: SchemaRefusal[],
|
|
227
|
+
refuse: (keyword: string, reason: string) => void,
|
|
228
|
+
): void {
|
|
229
|
+
const items = schema["items"];
|
|
230
|
+
if (isObject(items)) {
|
|
231
|
+
value.forEach((item, i) => {
|
|
232
|
+
this.check(doc, items, item, `${at}/${String(i)}`, found);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (typeof schema["minItems"] === "number" && value.length < schema["minItems"]) {
|
|
236
|
+
refuse(
|
|
237
|
+
"minItems",
|
|
238
|
+
`the array holds ${String(value.length)} items, fewer than ${String(schema["minItems"])}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (typeof schema["maxItems"] === "number" && value.length > schema["maxItems"]) {
|
|
242
|
+
refuse(
|
|
243
|
+
"maxItems",
|
|
244
|
+
`the array holds ${String(value.length)} items, more than ${String(schema["maxItems"])}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (schema["uniqueItems"] === true && new Set(value.map(canonical)).size !== value.length) {
|
|
248
|
+
refuse("uniqueItems", "the array lists one value twice");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private checkApplicators(
|
|
253
|
+
doc: string,
|
|
254
|
+
schema: Schema,
|
|
255
|
+
value: unknown,
|
|
256
|
+
at: string,
|
|
257
|
+
found: SchemaRefusal[],
|
|
258
|
+
refuse: (keyword: string, reason: string) => void,
|
|
259
|
+
): void {
|
|
260
|
+
const list = (keyword: string): readonly Schema[] =>
|
|
261
|
+
Array.isArray(schema[keyword]) ? (schema[keyword] as Schema[]) : [];
|
|
262
|
+
for (const each of list("allOf")) {
|
|
263
|
+
this.check(doc, each, value, at, found);
|
|
264
|
+
}
|
|
265
|
+
const matching = (keyword: string): number =>
|
|
266
|
+
list(keyword).filter((each) => this.matches(doc, each, value, at)).length;
|
|
267
|
+
if (schema["anyOf"] !== undefined && matching("anyOf") === 0) {
|
|
268
|
+
refuse("anyOf", "the value matches none of the alternatives");
|
|
269
|
+
}
|
|
270
|
+
if (schema["oneOf"] !== undefined && matching("oneOf") !== 1) {
|
|
271
|
+
refuse(
|
|
272
|
+
"oneOf",
|
|
273
|
+
`the value matches ${String(matching("oneOf"))} of the alternatives, want exactly one`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (isObject(schema["not"]) && this.matches(doc, schema["not"], value, at)) {
|
|
277
|
+
refuse("not", "the value matches a schema it must not match");
|
|
278
|
+
}
|
|
279
|
+
if (isObject(schema["if"])) {
|
|
280
|
+
const branch = this.matches(doc, schema["if"], value, at) ? schema["then"] : schema["else"];
|
|
281
|
+
if (isObject(branch)) {
|
|
282
|
+
this.check(doc, branch, value, at, found);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A validator over one set of schema documents, each named as a `$ref` names it, rooted
|
|
290
|
+
* at one of them: what it refuses in a value, none for an instance.
|
|
291
|
+
*/
|
|
292
|
+
export function schemaRefusals(
|
|
293
|
+
documents: Documents,
|
|
294
|
+
root: string,
|
|
295
|
+
): (value: unknown) => readonly SchemaRefusal[] {
|
|
296
|
+
const checker = new Checker(documents);
|
|
297
|
+
const top = documents[root];
|
|
298
|
+
if (top === undefined) {
|
|
299
|
+
throw new Error(`the schema set holds no document ${root}`);
|
|
300
|
+
}
|
|
301
|
+
return (value) => {
|
|
302
|
+
const found: SchemaRefusal[] = [];
|
|
303
|
+
checker.check(root, top, value, "", found);
|
|
304
|
+
return found;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const reportSchema = schemaRefusals(REPORT_SCHEMAS, "report.schema.json");
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* What the report schema refuses in one report document, none for an instance, and, as
|
|
312
|
+
* no schema keyword can state it, a components listing that is not one entry per
|
|
313
|
+
* component a finding or an edge evaluation's finding names.
|
|
314
|
+
*/
|
|
315
|
+
export function reportRefusals(document: unknown): readonly SchemaRefusal[] {
|
|
316
|
+
const found = reportSchema(document);
|
|
317
|
+
return found.length > 0 ? found : listingRefusals(document as Report);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The components listing held to the components the findings name, once each. */
|
|
321
|
+
function listingRefusals(report: Report): readonly SchemaRefusal[] {
|
|
322
|
+
if (report.components === undefined) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
const refuse = (pointer: string, reason: string): SchemaRefusal[] => [
|
|
326
|
+
{ pointer, keyword: "components", reason },
|
|
327
|
+
];
|
|
328
|
+
const listed = new Set<string>();
|
|
329
|
+
for (const [i, entry] of report.components.entries()) {
|
|
330
|
+
if (listed.has(entry.id)) {
|
|
331
|
+
return refuse(
|
|
332
|
+
`/components/${String(i)}`,
|
|
333
|
+
`the entry lists component ${entry.id} a second time`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
listed.add(entry.id);
|
|
337
|
+
}
|
|
338
|
+
const named = [
|
|
339
|
+
...report.findings.map(
|
|
340
|
+
(finding, i) => [`/findings/${String(i)}`, finding.component.id] as const,
|
|
341
|
+
),
|
|
342
|
+
...report.edge_evaluations.flatMap((one, i) =>
|
|
343
|
+
one.finding === undefined
|
|
344
|
+
? []
|
|
345
|
+
: [[`/edge_evaluations/${String(i)}/finding`, one.finding.component.id] as const],
|
|
346
|
+
),
|
|
347
|
+
];
|
|
348
|
+
for (const [pointer, id] of named) {
|
|
349
|
+
if (!listed.has(id)) {
|
|
350
|
+
return refuse(
|
|
351
|
+
pointer,
|
|
352
|
+
`the finding names component ${id}, which the components listing lacks`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const ids = new Set(named.map(([, id]) => id));
|
|
357
|
+
for (const [i, entry] of report.components.entries()) {
|
|
358
|
+
if (!ids.has(entry.id)) {
|
|
359
|
+
return refuse(
|
|
360
|
+
`/components/${String(i)}`,
|
|
361
|
+
`the entry lists component ${entry.id}, which no finding names`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return [];
|
|
366
|
+
}
|