modelfunction-cli 0.2.0 → 0.4.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 +5 -5
- package/lib/cases.js +32 -13
- package/lib/cluster.js +57 -2
- package/lib/js-tools.js +954 -0
- package/lib/lint.js +0 -0
- package/lib/mcp-server.js +1 -0
- package/lib/scan.js +80 -14
- package/lib/suite.js +20 -3
- package/mf.js +12 -5
- package/package.json +1 -1
package/lib/js-tools.js
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// Static extraction of tool definitions from JS/TS source. This never executes
|
|
2
|
+
// the file: it parses the subset of expressions that real registries use —
|
|
3
|
+
// object/array literals, `const` references to those literals, and the TypeBox
|
|
4
|
+
// (`Type.Object`) / zod (`z.object`) builders. Anything else is reported as
|
|
5
|
+
// unresolved instead of being guessed, because a wrong schema is worse than a
|
|
6
|
+
// missing tool.
|
|
7
|
+
const MAX_VALUE_DEPTH = 24;
|
|
8
|
+
const MAX_RESOLVE_DEPTH = 8;
|
|
9
|
+
const MAX_ARRAY_ATTEMPTS = 300;
|
|
10
|
+
|
|
11
|
+
const PREFILTER = /inputSchema|parameters\s*:|registerTool|\.tool\s*\(|buildTool|defineTool|createTool|Type\.Object|[tz]\.object/;
|
|
12
|
+
const TOOL_BUILDERS = new Set(["buildTool", "defineTool", "createTool", "makeTool"]);
|
|
13
|
+
// `async () => {}` and `function () {}` otherwise parse as a call named
|
|
14
|
+
// "async"/"function" and swallow the rest of the argument list.
|
|
15
|
+
const RESERVED = new Set([
|
|
16
|
+
"async", "await", "function", "new", "return", "if", "else", "for", "while",
|
|
17
|
+
"do", "switch", "case", "try", "catch", "finally", "throw", "typeof", "void",
|
|
18
|
+
"delete", "in", "of", "instanceof", "class", "const", "let", "var", "export",
|
|
19
|
+
"import", "yield", "this", "super"
|
|
20
|
+
]);
|
|
21
|
+
const SCHEMA_KEYS = ["inputSchema", "parameters", "input", "parametersSchema", "schema"];
|
|
22
|
+
|
|
23
|
+
export function extractJsTools(src, { externals, onUnresolvedName } = {}) {
|
|
24
|
+
if (typeof src !== "string" || !PREFILTER.test(src)) return [];
|
|
25
|
+
const ctx = { src, bindings: collectBindings(src), parsed: new Map(), externals, onUnresolvedName };
|
|
26
|
+
const tools = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const push = (tool) => {
|
|
29
|
+
if (!tool || !tool.name || seen.has(tool.name)) return;
|
|
30
|
+
seen.add(tool.name);
|
|
31
|
+
tools.push(tool);
|
|
32
|
+
};
|
|
33
|
+
let attempts = 0;
|
|
34
|
+
|
|
35
|
+
for (const match of src.matchAll(/([A-Za-z_$][\w$]*)\.(tool|registerTool)\s*\(/g)) {
|
|
36
|
+
const parsed = parseArgs(src, match.index + match[0].length - 1);
|
|
37
|
+
if (parsed) push(toolFromArgs(parsed.items, ctx));
|
|
38
|
+
}
|
|
39
|
+
for (const match of src.matchAll(/(?<![.\w$])registerTool\s*\(/g)) {
|
|
40
|
+
const parsed = parseArgs(src, match.index + match[0].length - 1);
|
|
41
|
+
if (parsed) push(toolFromArgs(parsed.items, ctx));
|
|
42
|
+
}
|
|
43
|
+
for (const match of src.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
44
|
+
if (!TOOL_BUILDERS.has(match[1])) continue;
|
|
45
|
+
const parsed = parseArgs(src, match.index + match[0].length - 1);
|
|
46
|
+
if (!parsed || !parsed.items[0] || parsed.items[0].t !== "obj") continue;
|
|
47
|
+
push(toolFromObject(parsed.items[0], ctx));
|
|
48
|
+
}
|
|
49
|
+
for (const match of src.matchAll(/(?:export\s+)?(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*(?::[^=\n]{0,200})?=\s*\{/g)) {
|
|
50
|
+
const parsed = parseValue(src, match.index + match[0].length - 1, 0);
|
|
51
|
+
if (parsed && parsed.node.t === "obj") push(toolFromObject(parsed.node, ctx));
|
|
52
|
+
}
|
|
53
|
+
for (const match of src.matchAll(/\[\s*\{/g)) {
|
|
54
|
+
if (attempts >= MAX_ARRAY_ATTEMPTS) break;
|
|
55
|
+
attempts += 1;
|
|
56
|
+
const parsed = parseValue(src, match.index, 0);
|
|
57
|
+
if (!parsed || parsed.node.t !== "arr") continue;
|
|
58
|
+
for (const item of parsed.node.items) {
|
|
59
|
+
if (item && item.t === "obj") push(toolFromObject(item, ctx));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return tools;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toolFromArgs(args, ctx) {
|
|
66
|
+
const name = stringOf(args[0], ctx);
|
|
67
|
+
if (!name) return null;
|
|
68
|
+
let description = "";
|
|
69
|
+
let parameters = null;
|
|
70
|
+
for (const arg of args.slice(1)) {
|
|
71
|
+
if (!arg) continue;
|
|
72
|
+
if (arg.t === "str") {
|
|
73
|
+
if (!description) description = arg.value;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg.t !== "obj") continue;
|
|
77
|
+
const fromObject = toolFromObject(arg, ctx, { name, description });
|
|
78
|
+
if (fromObject) return fromObject;
|
|
79
|
+
}
|
|
80
|
+
for (const arg of args.slice(1)) {
|
|
81
|
+
if (!arg) continue;
|
|
82
|
+
const { schema, resolved } = schemaOf(arg, ctx);
|
|
83
|
+
if (!resolved || !isSchema(schema)) continue;
|
|
84
|
+
parameters = schema;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (!parameters) return null;
|
|
88
|
+
return { name, description, parameters };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toolFromObject(node, ctx, preset = {}) {
|
|
92
|
+
const name = preset.name ?? stringOf(node.props.get("name"), ctx) ?? stringOf(node.props.get("toolName"), ctx);
|
|
93
|
+
if (!name) return undefined;
|
|
94
|
+
const description =
|
|
95
|
+
stringOf(node.props.get("description"), ctx) ??
|
|
96
|
+
stringOf(node.props.get("summary"), ctx) ??
|
|
97
|
+
preset.description ??
|
|
98
|
+
"";
|
|
99
|
+
let schemaNode = null;
|
|
100
|
+
for (const key of SCHEMA_KEYS) {
|
|
101
|
+
if (node.props.has(key)) {
|
|
102
|
+
schemaNode = node.props.get(key);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!schemaNode) return undefined;
|
|
107
|
+
const { schema, resolved } = schemaOf(schemaNode, ctx);
|
|
108
|
+
if (!resolved || !isSchema(schema)) return undefined;
|
|
109
|
+
// An empty schema with no description and no tool behaviour is a config
|
|
110
|
+
// object that happens to be shaped like a tool (`{ name: "mcp" }`).
|
|
111
|
+
const toolish =
|
|
112
|
+
node.props.has("call") ||
|
|
113
|
+
node.props.has("prompt") ||
|
|
114
|
+
node.props.has("userFacingName") ||
|
|
115
|
+
node.props.has("isEnabled") ||
|
|
116
|
+
node.props.has("searchHint");
|
|
117
|
+
if (!description && Object.keys(schema.properties ?? {}).length === 0 && !toolish) return undefined;
|
|
118
|
+
return { name, description, parameters: schema };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isSchema(value) {
|
|
122
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stringOf(node, ctx) {
|
|
126
|
+
if (!node) return null;
|
|
127
|
+
if (node.t === "str") return node.value;
|
|
128
|
+
if (node.t === "ident") {
|
|
129
|
+
// `name: CONFIG_TOOL_NAME` — a local const, or a repo-wide uppercase
|
|
130
|
+
// constant indexed by the scanner across files.
|
|
131
|
+
const local = resolveIdent(node.name, ctx);
|
|
132
|
+
if (local && local.t === "str") return local.value;
|
|
133
|
+
const external = ctx?.externals?.get(node.name);
|
|
134
|
+
if (typeof external === "string") return external;
|
|
135
|
+
ctx?.onUnresolvedName?.(node.name);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
if (node.t === "obj") {
|
|
139
|
+
const value = node.props.get("value");
|
|
140
|
+
if (value && value.t === "str") return value.value;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- schema construction -------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function schemaOf(node, ctx, depth = 0) {
|
|
148
|
+
if (!node || depth > MAX_VALUE_DEPTH) return { schema: {}, resolved: false };
|
|
149
|
+
if (node.t === "ident") {
|
|
150
|
+
const resolvedNode = resolveIdent(node.name, ctx, depth);
|
|
151
|
+
if (!resolvedNode) return { schema: {}, resolved: false };
|
|
152
|
+
return schemaOf(resolvedNode, ctx, depth + 1);
|
|
153
|
+
}
|
|
154
|
+
if (node.t === "obj") {
|
|
155
|
+
if (node.props.has("type") || node.props.has("properties") || node.props.has("items")) {
|
|
156
|
+
return { schema: jsonSchemaLiteral(node, ctx, depth), resolved: true };
|
|
157
|
+
}
|
|
158
|
+
// MCP SDK shape argument: server.tool("x", "desc", { query: z.string() }).
|
|
159
|
+
const rawShape = rawShapeSchema(node, ctx, depth);
|
|
160
|
+
return rawShape ? { schema: rawShape, resolved: true } : { schema: {}, resolved: false };
|
|
161
|
+
}
|
|
162
|
+
if (node.t !== "call") return { schema: {}, resolved: false };
|
|
163
|
+
|
|
164
|
+
const args = node.args ?? [];
|
|
165
|
+
const callee = node.callee;
|
|
166
|
+
let schema = null;
|
|
167
|
+
let optional = false;
|
|
168
|
+
|
|
169
|
+
switch (callee) {
|
|
170
|
+
case "Type.Object":
|
|
171
|
+
case "t.Object":
|
|
172
|
+
case "z.object":
|
|
173
|
+
case "z.strictObject":
|
|
174
|
+
case "z.looseObject":
|
|
175
|
+
schema = objectSchema(args[0], ctx, depth);
|
|
176
|
+
break;
|
|
177
|
+
case "Type.String":
|
|
178
|
+
case "t.String":
|
|
179
|
+
case "z.string":
|
|
180
|
+
schema = { type: "string" };
|
|
181
|
+
break;
|
|
182
|
+
case "Type.Number":
|
|
183
|
+
case "t.Number":
|
|
184
|
+
case "z.number":
|
|
185
|
+
schema = { type: "number" };
|
|
186
|
+
break;
|
|
187
|
+
case "Type.Integer":
|
|
188
|
+
case "t.Integer":
|
|
189
|
+
case "z.int":
|
|
190
|
+
schema = { type: "integer" };
|
|
191
|
+
break;
|
|
192
|
+
case "z.coerce.number":
|
|
193
|
+
schema = { type: "number" };
|
|
194
|
+
break;
|
|
195
|
+
case "z.coerce.string":
|
|
196
|
+
schema = { type: "string" };
|
|
197
|
+
break;
|
|
198
|
+
case "z.coerce.boolean":
|
|
199
|
+
schema = { type: "boolean" };
|
|
200
|
+
break;
|
|
201
|
+
case "Type.Boolean":
|
|
202
|
+
case "t.Boolean":
|
|
203
|
+
case "z.boolean":
|
|
204
|
+
schema = { type: "boolean" };
|
|
205
|
+
break;
|
|
206
|
+
case "Type.Array":
|
|
207
|
+
case "t.Array":
|
|
208
|
+
case "z.array": {
|
|
209
|
+
const item = schemaOf(args[0], ctx, depth + 1);
|
|
210
|
+
schema = { type: "array", items: item.schema };
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "Type.Optional":
|
|
214
|
+
case "z.optional": {
|
|
215
|
+
const inner = schemaOf(args[0], ctx, depth + 1);
|
|
216
|
+
return { ...inner, optional: true };
|
|
217
|
+
}
|
|
218
|
+
case "Type.Literal":
|
|
219
|
+
case "t.Literal":
|
|
220
|
+
case "z.literal": {
|
|
221
|
+
const value = plainOf(args[0]);
|
|
222
|
+
schema = value === undefined ? {} : { enum: [value] };
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "z.enum": {
|
|
226
|
+
const values = plainOf(args[0]);
|
|
227
|
+
schema = Array.isArray(values) ? { type: "string", enum: values } : {};
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "Type.Union":
|
|
231
|
+
case "t.Union":
|
|
232
|
+
case "z.union": {
|
|
233
|
+
const nodes = args.length === 1 && args[0] && args[0].t === "arr" ? args[0].items : args;
|
|
234
|
+
const literals = nodes.map((item) => plainOf(item));
|
|
235
|
+
if (literals.length > 0 && literals.every((item) => item !== undefined)) {
|
|
236
|
+
schema = { enum: literals };
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
// z.union([z.string(), z.boolean()]) — keep the types we can name.
|
|
240
|
+
const types = [
|
|
241
|
+
...new Set(nodes.map((item) => schemaOf(item, ctx, depth + 1).schema?.type).filter((t) => typeof t === "string"))
|
|
242
|
+
];
|
|
243
|
+
schema = types.length > 0 ? { type: types.length === 1 ? types[0] : types } : {};
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "lazySchema":
|
|
247
|
+
case "memoize":
|
|
248
|
+
case "lazy":
|
|
249
|
+
case "once":
|
|
250
|
+
return schemaOf(args[0], ctx, depth + 1);
|
|
251
|
+
case "Type.Record":
|
|
252
|
+
case "t.Record":
|
|
253
|
+
case "z.record":
|
|
254
|
+
schema = { type: "object", additionalProperties: true };
|
|
255
|
+
break;
|
|
256
|
+
case "Type.Any":
|
|
257
|
+
case "Type.Unknown":
|
|
258
|
+
case "z.any":
|
|
259
|
+
case "z.unknown":
|
|
260
|
+
schema = {};
|
|
261
|
+
break;
|
|
262
|
+
default: {
|
|
263
|
+
// Factory call: `inputSchema()` where
|
|
264
|
+
// `inputSchema = lazySchema(() => z.object({...}))`.
|
|
265
|
+
if (/^[A-Za-z_$][\w$]*$/.test(callee)) {
|
|
266
|
+
const target = resolveIdent(callee, ctx, depth);
|
|
267
|
+
if (target) return schemaOf(target, ctx, depth + 1);
|
|
268
|
+
}
|
|
269
|
+
// Wrapper helpers take exactly one schema and add semantics to it
|
|
270
|
+
// (`semanticNumber(z.number().int())`). Unwrap only when exactly one
|
|
271
|
+
// argument resolves to a schema, so arrays of schemas are left alone.
|
|
272
|
+
const inner = args
|
|
273
|
+
.filter(Boolean)
|
|
274
|
+
.map((arg) => schemaOf(arg, ctx, depth + 1))
|
|
275
|
+
.filter((candidate) => candidate.resolved && isSchema(candidate.schema));
|
|
276
|
+
if (inner.length === 1) return inner[0];
|
|
277
|
+
return { schema: {}, resolved: false };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// TypeBox carries options as the first object argument, e.g.
|
|
282
|
+
// Type.String({ description: "File path", format: "uuid" }).
|
|
283
|
+
if (schema && args[0] && args[0].t === "obj" && /^(Type|t)\./.test(callee)) {
|
|
284
|
+
applyOptions(schema, args[0]);
|
|
285
|
+
}
|
|
286
|
+
for (const chained of node.calls ?? []) {
|
|
287
|
+
if (applyMethod(schema, chained) === "optional") optional = true;
|
|
288
|
+
}
|
|
289
|
+
return { schema, resolved: true, optional };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function rawShapeSchema(node, ctx, depth) {
|
|
293
|
+
const properties = {};
|
|
294
|
+
const required = [];
|
|
295
|
+
for (const [key, valueNode] of node.props) {
|
|
296
|
+
if (!valueNode || !["call", "ident", "obj"].includes(valueNode.t)) return null;
|
|
297
|
+
const { schema, resolved, optional } = schemaOf(valueNode, ctx, depth + 1);
|
|
298
|
+
if (!resolved) return null;
|
|
299
|
+
properties[key] = schema;
|
|
300
|
+
if (!optional) required.push(key);
|
|
301
|
+
}
|
|
302
|
+
if (Object.keys(properties).length === 0) return null;
|
|
303
|
+
return { type: "object", properties, required };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function objectSchema(node, ctx, depth) {
|
|
307
|
+
if (!node) return { type: "object", properties: {} };
|
|
308
|
+
const resolved = node.t === "ident" ? resolveIdent(node.name, ctx, depth) : node;
|
|
309
|
+
if (!resolved || resolved.t !== "obj") return { type: "object", properties: {} };
|
|
310
|
+
const properties = {};
|
|
311
|
+
const required = [];
|
|
312
|
+
for (const [key, valueNode] of resolved.props) {
|
|
313
|
+
const { schema, optional } = schemaOf(valueNode, ctx, depth + 1);
|
|
314
|
+
properties[key] = schema;
|
|
315
|
+
if (!optional) required.push(key);
|
|
316
|
+
}
|
|
317
|
+
return { type: "object", properties, required };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function jsonSchemaLiteral(node, ctx, depth) {
|
|
321
|
+
const schema = {};
|
|
322
|
+
const type = plainOf(node.props.get("type"));
|
|
323
|
+
if (typeof type === "string") schema.type = type;
|
|
324
|
+
const description = plainOf(node.props.get("description"));
|
|
325
|
+
if (typeof description === "string") schema.description = description;
|
|
326
|
+
const format = plainOf(node.props.get("format"));
|
|
327
|
+
if (typeof format === "string") schema.format = format;
|
|
328
|
+
const enumValues = plainOf(node.props.get("enum"));
|
|
329
|
+
if (Array.isArray(enumValues)) schema.enum = enumValues;
|
|
330
|
+
for (const key of ["default", "minimum", "maximum", "minLength", "maxLength", "pattern"]) {
|
|
331
|
+
const value = plainOf(node.props.get(key));
|
|
332
|
+
if (value !== undefined) schema[key] = value;
|
|
333
|
+
}
|
|
334
|
+
if (node.props.has("items")) {
|
|
335
|
+
const item = schemaOf(node.props.get("items"), ctx, depth + 1);
|
|
336
|
+
schema.items = item.schema;
|
|
337
|
+
}
|
|
338
|
+
if (node.props.has("properties")) {
|
|
339
|
+
const raw = node.props.get("properties");
|
|
340
|
+
const resolved = raw && raw.t === "ident" ? resolveIdent(raw.name, ctx, depth) : raw;
|
|
341
|
+
if (resolved && resolved.t === "obj") {
|
|
342
|
+
schema.type = schema.type ?? "object";
|
|
343
|
+
schema.properties = objectSchema(resolved, ctx, depth).properties;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (node.props.has("required")) {
|
|
347
|
+
const required = plainOf(node.props.get("required"));
|
|
348
|
+
if (Array.isArray(required)) schema.required = required;
|
|
349
|
+
} else if (schema.properties) {
|
|
350
|
+
schema.required = Object.keys(schema.properties);
|
|
351
|
+
}
|
|
352
|
+
if (schema.type === "object" && !schema.properties) schema.properties = {};
|
|
353
|
+
return schema;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function applyOptions(schema, optionsNode) {
|
|
357
|
+
for (const [key, valueNode] of optionsNode.props) {
|
|
358
|
+
if (key === "description") {
|
|
359
|
+
const value = plainOf(valueNode);
|
|
360
|
+
if (typeof value === "string") schema.description = value;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (["format", "default", "minimum", "maximum", "minLength", "maxLength", "pattern"].includes(key)) {
|
|
364
|
+
const value = plainOf(valueNode);
|
|
365
|
+
if (value !== undefined) schema[key] = value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function applyMethod(schema, chained) {
|
|
371
|
+
const [first] = chained.args ?? [];
|
|
372
|
+
switch (chained.name) {
|
|
373
|
+
case "describe": {
|
|
374
|
+
const value = plainOf(first);
|
|
375
|
+
if (typeof value === "string") schema.description = value;
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
case "optional":
|
|
379
|
+
return "optional";
|
|
380
|
+
case "default": {
|
|
381
|
+
const value = plainOf(first);
|
|
382
|
+
if (value !== undefined) schema.default = value;
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
case "min": {
|
|
386
|
+
const value = plainOf(first);
|
|
387
|
+
if (typeof value === "number") {
|
|
388
|
+
if (schema.type === "string") schema.minLength = value;
|
|
389
|
+
else schema.minimum = value;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
case "max": {
|
|
394
|
+
const value = plainOf(first);
|
|
395
|
+
if (typeof value === "number") {
|
|
396
|
+
if (schema.type === "string") schema.maxLength = value;
|
|
397
|
+
else schema.maximum = value;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
case "int":
|
|
402
|
+
schema.type = "integer";
|
|
403
|
+
return null;
|
|
404
|
+
case "uuid":
|
|
405
|
+
schema.format = "uuid";
|
|
406
|
+
return null;
|
|
407
|
+
default:
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function plainOf(node, depth = 0) {
|
|
413
|
+
if (!node || depth > MAX_VALUE_DEPTH) return undefined;
|
|
414
|
+
switch (node.t) {
|
|
415
|
+
case "str":
|
|
416
|
+
case "num":
|
|
417
|
+
case "bool":
|
|
418
|
+
return node.value;
|
|
419
|
+
case "null":
|
|
420
|
+
return null;
|
|
421
|
+
case "arr": {
|
|
422
|
+
const values = node.items.map((item) => plainOf(item, depth + 1));
|
|
423
|
+
return values.some((value) => value === undefined) ? undefined : values;
|
|
424
|
+
}
|
|
425
|
+
case "obj": {
|
|
426
|
+
if (node.props.has("type") || node.props.has("properties")) return undefined;
|
|
427
|
+
const value = {};
|
|
428
|
+
for (const [key, item] of node.props) {
|
|
429
|
+
const plain = plainOf(item, depth + 1);
|
|
430
|
+
if (plain === undefined) return undefined;
|
|
431
|
+
value[key] = plain;
|
|
432
|
+
}
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
default:
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---- bindings and identifier resolution ----------------------------------
|
|
441
|
+
|
|
442
|
+
function collectBindings(src) {
|
|
443
|
+
const bindings = new Map();
|
|
444
|
+
const pattern = /(?:^|[;\n{}])\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]{0,200})?=\s*/g;
|
|
445
|
+
for (const match of src.matchAll(pattern)) {
|
|
446
|
+
if (!bindings.has(match[1])) bindings.set(match[1], match.index + match[0].length);
|
|
447
|
+
}
|
|
448
|
+
return bindings;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function resolveIdent(name, ctx, depth = 0) {
|
|
452
|
+
if (depth > MAX_RESOLVE_DEPTH) return null;
|
|
453
|
+
if (ctx.parsed.has(name)) return ctx.parsed.get(name);
|
|
454
|
+
const start = ctx.bindings.get(name);
|
|
455
|
+
if (start === undefined) return null;
|
|
456
|
+
ctx.parsed.set(name, null);
|
|
457
|
+
const parsed = parseValue(ctx.src, start, 0);
|
|
458
|
+
const node = parsed ? parsed.node : null;
|
|
459
|
+
ctx.parsed.set(name, node);
|
|
460
|
+
return node;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- value parser --------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
function parseValue(src, start, depth) {
|
|
466
|
+
if (depth > MAX_VALUE_DEPTH) return null;
|
|
467
|
+
const i = skipTrivia(src, start);
|
|
468
|
+
const ch = src[i];
|
|
469
|
+
if (ch === undefined) return null;
|
|
470
|
+
if (ch === "{") return parseObject(src, i, depth);
|
|
471
|
+
if (ch === "[") return parseArray(src, i, depth);
|
|
472
|
+
if (ch === '"' || ch === "'" || ch === "`") return parseString(src, i);
|
|
473
|
+
if (ch === "-" || (ch >= "0" && ch <= "9")) return parseNumber(src, i);
|
|
474
|
+
if (ch === "(") return tryArrow(src, i, depth);
|
|
475
|
+
const ident = readIdent(src, i);
|
|
476
|
+
if (ident) {
|
|
477
|
+
const arrow = tryArrow(src, i, depth);
|
|
478
|
+
if (arrow) return arrow;
|
|
479
|
+
if (!RESERVED.has(ident.name)) return parseChain(src, i, depth);
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// `lazySchema(() => z.object({...}))` — the schema lives in the arrow body. A
|
|
485
|
+
// block body is a function, not a value, so it becomes unknown rather than a
|
|
486
|
+
// guess.
|
|
487
|
+
function tryArrow(src, start, depth) {
|
|
488
|
+
let bodyFrom;
|
|
489
|
+
if (src[start] === "(") {
|
|
490
|
+
const close = matchParen(src, start);
|
|
491
|
+
if (close === null) return null;
|
|
492
|
+
bodyFrom = close + 1;
|
|
493
|
+
} else {
|
|
494
|
+
const ident = readIdent(src, start);
|
|
495
|
+
if (!ident) return null;
|
|
496
|
+
bodyFrom = ident.end;
|
|
497
|
+
}
|
|
498
|
+
const arrowAt = skipTrivia(src, bodyFrom);
|
|
499
|
+
if (!src.startsWith("=>", arrowAt)) return null;
|
|
500
|
+
const bodyStart = skipTrivia(src, arrowAt + 2);
|
|
501
|
+
if (src[bodyStart] === "{") {
|
|
502
|
+
const end = skipBraced(src, bodyStart + 1);
|
|
503
|
+
return end === null ? null : { node: { t: "unknown" }, end };
|
|
504
|
+
}
|
|
505
|
+
return parseValue(src, bodyStart, depth + 1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function matchParen(src, start) {
|
|
509
|
+
let depth = 0;
|
|
510
|
+
let i = start;
|
|
511
|
+
while (i < src.length) {
|
|
512
|
+
const ch = src[i];
|
|
513
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
514
|
+
const parsed = parseString(src, i);
|
|
515
|
+
if (parsed) {
|
|
516
|
+
i = parsed.end;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (ch === "(") depth += 1;
|
|
521
|
+
else if (ch === ")") {
|
|
522
|
+
depth -= 1;
|
|
523
|
+
if (depth === 0) return i;
|
|
524
|
+
}
|
|
525
|
+
i += 1;
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function parseObject(src, start, depth) {
|
|
531
|
+
const props = new Map();
|
|
532
|
+
let i = skipTrivia(src, start + 1);
|
|
533
|
+
if (src[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
|
|
534
|
+
for (;;) {
|
|
535
|
+
i = skipTrivia(src, i);
|
|
536
|
+
// A trailing comma leaves us on the closing brace.
|
|
537
|
+
if (src[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
|
|
538
|
+
if (src.startsWith("...", i)) return null;
|
|
539
|
+
let key = null;
|
|
540
|
+
if (src[i] === '"' || src[i] === "'" || src[i] === "`") {
|
|
541
|
+
const parsed = parseString(src, i);
|
|
542
|
+
if (!parsed) return null;
|
|
543
|
+
key = parsed.node.value;
|
|
544
|
+
i = parsed.end;
|
|
545
|
+
} else {
|
|
546
|
+
const ident = readIdent(src, i);
|
|
547
|
+
if (ident) {
|
|
548
|
+
key = ident.name;
|
|
549
|
+
i = ident.end;
|
|
550
|
+
} else if (src[i] >= "0" && src[i] <= "9") {
|
|
551
|
+
const num = parseNumber(src, i);
|
|
552
|
+
if (!num) return null;
|
|
553
|
+
key = String(num.node.value);
|
|
554
|
+
i = num.end;
|
|
555
|
+
} else {
|
|
556
|
+
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
i = skipTrivia(src, i);
|
|
561
|
+
if (src[i] !== ":") {
|
|
562
|
+
// Property shorthand: `{ inputSchema, name, description }` refers to a
|
|
563
|
+
// local binding of the same name.
|
|
564
|
+
if (src[i] === "," || src[i] === "}") {
|
|
565
|
+
props.set(key, { t: "ident", name: key });
|
|
566
|
+
if (src[i] === ",") {
|
|
567
|
+
i += 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
return { node: { t: "obj", props }, end: i + 1 };
|
|
571
|
+
}
|
|
572
|
+
// Accessors: `get inputSchema(): T { return inputSchema(); }`. Only a
|
|
573
|
+
// single-return body is taken; a conditional getter is left unknown
|
|
574
|
+
// rather than guessed from its first branch.
|
|
575
|
+
if ((key === "get" || key === "set") && /[A-Za-z_$]/.test(src[i] ?? "")) {
|
|
576
|
+
const accessor = readIdent(src, i);
|
|
577
|
+
const parenAt = skipTrivia(src, accessor.end);
|
|
578
|
+
if (src[parenAt] === "(") {
|
|
579
|
+
const params = parseArgs(src, parenAt);
|
|
580
|
+
if (!params) return null;
|
|
581
|
+
let braceAt = skipTrivia(src, params.end);
|
|
582
|
+
// `get inputSchema(): InputSchema {` — skip a simple return type.
|
|
583
|
+
if (src[braceAt] === ":") braceAt = findBrace(src, braceAt + 1);
|
|
584
|
+
if (src[braceAt] !== "{") return null;
|
|
585
|
+
const bodyEnd = skipBraced(src, braceAt + 1);
|
|
586
|
+
if (bodyEnd === null) return null;
|
|
587
|
+
props.set(accessor.name, getterValue(src, braceAt + 1, bodyEnd, depth));
|
|
588
|
+
i = skipTrivia(src, bodyEnd);
|
|
589
|
+
if (src[i] === ",") {
|
|
590
|
+
i += 1;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (src[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Shorthand methods and getters: `async call(args) { ... }`, `get x() {}`.
|
|
598
|
+
const shorthand = src[i] === "(" || src[i] === "<" || src[i] === "[" || /[A-Za-z_$]/.test(src[i] ?? "");
|
|
599
|
+
if (!shorthand) {
|
|
600
|
+
return null; }
|
|
601
|
+
const skipped = skipToValueEnd(src, i);
|
|
602
|
+
if (skipped === null) {
|
|
603
|
+
return null; }
|
|
604
|
+
props.set(key, { t: "unknown" });
|
|
605
|
+
i = skipped;
|
|
606
|
+
if (src[i] === ",") {
|
|
607
|
+
i += 1;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (src[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const value = parseValue(src, i + 1, depth + 1);
|
|
614
|
+
const after = value ? skipTrivia(src, value.end) : -1;
|
|
615
|
+
if (value && (src[after] === "," || src[after] === "}")) {
|
|
616
|
+
props.set(key, value.node);
|
|
617
|
+
i = after;
|
|
618
|
+
} else {
|
|
619
|
+
// Unparseable or only partially parsed expression (`x => a || b`):
|
|
620
|
+
// keep the keys we understand and skip this value.
|
|
621
|
+
const skipped = skipToValueEnd(src, i + 1);
|
|
622
|
+
if (skipped === null) return null;
|
|
623
|
+
props.set(key, { t: "unknown" });
|
|
624
|
+
i = skipped;
|
|
625
|
+
}
|
|
626
|
+
if (src[i] === ",") {
|
|
627
|
+
i += 1;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (src[i] === "}") return { node: { t: "obj", props }, end: i + 1 };
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function findBrace(src, start) {
|
|
636
|
+
let i = start;
|
|
637
|
+
while (i < src.length) {
|
|
638
|
+
const ch = src[i];
|
|
639
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
640
|
+
const parsed = parseString(src, i);
|
|
641
|
+
if (parsed) {
|
|
642
|
+
i = parsed.end;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (ch === "{") return i;
|
|
647
|
+
if (ch === ";" || ch === "}") return i;
|
|
648
|
+
i += 1;
|
|
649
|
+
}
|
|
650
|
+
return i;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function getterValue(src, start, end, depth) {
|
|
654
|
+
const body = src.slice(start, end);
|
|
655
|
+
const returns = body.match(/\breturn\b/g) ?? [];
|
|
656
|
+
if (returns.length !== 1) return { t: "unknown" };
|
|
657
|
+
const at = start + body.indexOf("return") + "return".length;
|
|
658
|
+
const parsed = parseValue(src, at, depth + 1);
|
|
659
|
+
return parsed ? parsed.node : { t: "unknown" };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function skipToValueEnd(src, start) {
|
|
663
|
+
let i = start;
|
|
664
|
+
let depth = 0;
|
|
665
|
+
while (i < src.length) {
|
|
666
|
+
const ch = src[i];
|
|
667
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
668
|
+
const parsed = parseString(src, i);
|
|
669
|
+
if (parsed) {
|
|
670
|
+
i = parsed.end;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
675
|
+
const nl = src.indexOf("\n", i);
|
|
676
|
+
i = nl < 0 ? src.length : nl + 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
680
|
+
else if (ch === ")" || ch === "]") {
|
|
681
|
+
if (depth > 0) depth -= 1;
|
|
682
|
+
} else if (ch === "}") {
|
|
683
|
+
if (depth === 0) return i;
|
|
684
|
+
depth -= 1;
|
|
685
|
+
} else if (ch === "," && depth === 0) return i;
|
|
686
|
+
i += 1;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function parseArray(src, start, depth) {
|
|
692
|
+
const items = [];
|
|
693
|
+
let i = skipTrivia(src, start + 1);
|
|
694
|
+
if (src[i] === "]") return { node: { t: "arr", items }, end: i + 1 };
|
|
695
|
+
for (;;) {
|
|
696
|
+
const value = parseValue(src, i, depth + 1);
|
|
697
|
+
if (!value) return null;
|
|
698
|
+
items.push(value.node);
|
|
699
|
+
i = skipTrivia(src, value.end);
|
|
700
|
+
if (src[i] === ",") {
|
|
701
|
+
i = skipTrivia(src, i + 1);
|
|
702
|
+
if (src[i] === "]") return { node: { t: "arr", items }, end: i + 1 };
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (src[i] === "]") return { node: { t: "arr", items }, end: i + 1 };
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function parseString(src, start) {
|
|
711
|
+
const quote = src[start];
|
|
712
|
+
if (quote === "`") return parseTemplate(src, start);
|
|
713
|
+
let i = start + 1;
|
|
714
|
+
let value = "";
|
|
715
|
+
while (i < src.length) {
|
|
716
|
+
const ch = src[i];
|
|
717
|
+
if (ch === "\\") {
|
|
718
|
+
const next = src[i + 1];
|
|
719
|
+
const map = { n: "\n", t: "\t", r: "\r", b: "\b", f: "\f", v: "\v", "0": "\0" };
|
|
720
|
+
value += map[next] ?? next ?? "";
|
|
721
|
+
i += 2;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (ch === quote) return { node: { t: "str", value }, end: i + 1 };
|
|
725
|
+
if (ch === "\n") return null;
|
|
726
|
+
value += ch;
|
|
727
|
+
i += 1;
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Template literals appear inside handler bodies (`Slept for ${n} seconds`).
|
|
733
|
+
// Their end must be found structurally: a naive scan confuses the closing and
|
|
734
|
+
// opening backticks of two different templates and unbalances the object parse.
|
|
735
|
+
function parseTemplate(src, start) {
|
|
736
|
+
let i = start + 1;
|
|
737
|
+
let value = "";
|
|
738
|
+
let dynamic = false;
|
|
739
|
+
while (i < src.length) {
|
|
740
|
+
const ch = src[i];
|
|
741
|
+
if (ch === "\\") {
|
|
742
|
+
value += src[i + 1] ?? "";
|
|
743
|
+
i += 2;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (ch === "`") {
|
|
747
|
+
return dynamic ? { node: { t: "unknown" }, end: i + 1 } : { node: { t: "str", value }, end: i + 1 };
|
|
748
|
+
}
|
|
749
|
+
if (ch === "$" && src[i + 1] === "{") {
|
|
750
|
+
dynamic = true;
|
|
751
|
+
const end = skipBraced(src, i + 2);
|
|
752
|
+
if (end === null) return null;
|
|
753
|
+
i = end;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
value += ch;
|
|
757
|
+
i += 1;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function skipBraced(src, start) {
|
|
763
|
+
let depth = 1;
|
|
764
|
+
let i = start;
|
|
765
|
+
while (i < src.length) {
|
|
766
|
+
const ch = src[i];
|
|
767
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
768
|
+
const parsed = parseString(src, i);
|
|
769
|
+
if (parsed) {
|
|
770
|
+
i = parsed.end;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (ch === "{") depth += 1;
|
|
775
|
+
else if (ch === "}") {
|
|
776
|
+
depth -= 1;
|
|
777
|
+
if (depth === 0) return i + 1;
|
|
778
|
+
}
|
|
779
|
+
i += 1;
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function parseNumber(src, start) {
|
|
785
|
+
// Numeric separators (`50_000`) are common in real tool defaults.
|
|
786
|
+
const match = /^-?\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-]?\d+)?/i.exec(src.slice(start, start + 40));
|
|
787
|
+
if (!match) return null;
|
|
788
|
+
return { node: { t: "num", value: Number(match[0].replace(/_/g, "")) }, end: start + match[0].length };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function parseChain(src, start, depth) {
|
|
792
|
+
let i = start;
|
|
793
|
+
const segments = [];
|
|
794
|
+
const head = readIdent(src, i);
|
|
795
|
+
if (!head) return null;
|
|
796
|
+
segments.push(head.name);
|
|
797
|
+
i = head.end;
|
|
798
|
+
for (;;) {
|
|
799
|
+
const j = skipTrivia(src, i);
|
|
800
|
+
if (src[j] === ".") {
|
|
801
|
+
const next = readIdent(src, j + 1);
|
|
802
|
+
if (!next) break;
|
|
803
|
+
segments.push(next.name);
|
|
804
|
+
i = next.end;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (src[j] === "(") {
|
|
808
|
+
const parsed = parseArgs(src, j);
|
|
809
|
+
if (!parsed) return null;
|
|
810
|
+
const calls = [];
|
|
811
|
+
let end = parsed.end;
|
|
812
|
+
for (;;) {
|
|
813
|
+
const k = skipTrivia(src, end);
|
|
814
|
+
if (src[k] !== ".") break;
|
|
815
|
+
const method = readIdent(src, k + 1);
|
|
816
|
+
if (!method) break;
|
|
817
|
+
const l = skipTrivia(src, method.end);
|
|
818
|
+
if (src[l] !== "(") break;
|
|
819
|
+
const methodArgs = parseArgs(src, l);
|
|
820
|
+
if (!methodArgs) break;
|
|
821
|
+
calls.push({ name: method.name, args: methodArgs.items });
|
|
822
|
+
end = methodArgs.end;
|
|
823
|
+
}
|
|
824
|
+
return { node: { t: "call", callee: segments.join("."), args: parsed.items, calls }, end };
|
|
825
|
+
}
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
return { node: { t: "ident", name: segments.join(".") }, end: i };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function parseArgs(src, start) {
|
|
832
|
+
const items = [];
|
|
833
|
+
let i = skipTrivia(src, start + 1);
|
|
834
|
+
if (src[i] === ")") return { items, end: i + 1 };
|
|
835
|
+
for (;;) {
|
|
836
|
+
let value = parseValue(src, i, 0);
|
|
837
|
+
let after = value ? skipTrivia(src, value.end) : -1;
|
|
838
|
+
if (value) {
|
|
839
|
+
// TypeScript wrappers: `{...} satisfies ToolDef<...>` and `x as T`.
|
|
840
|
+
if (isTypeWrapperAt(src, after)) {
|
|
841
|
+
const skipped = skipTypeExpression(src, after);
|
|
842
|
+
if (skipped === null) return null;
|
|
843
|
+
after = skipped;
|
|
844
|
+
}
|
|
845
|
+
if (src[after] === "," || src[after] === ")") {
|
|
846
|
+
items.push(value.node);
|
|
847
|
+
i = after;
|
|
848
|
+
} else {
|
|
849
|
+
// Binary expressions (`a || b`, `a ?? b`) parse only their first
|
|
850
|
+
// operand; treat the whole argument as unknown instead.
|
|
851
|
+
value = null;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (!value) {
|
|
855
|
+
const skipped = skipArgument(src, i);
|
|
856
|
+
if (skipped === null) return null;
|
|
857
|
+
items.push(null);
|
|
858
|
+
i = skipped;
|
|
859
|
+
}
|
|
860
|
+
if (src[i] === ",") {
|
|
861
|
+
i = skipTrivia(src, i + 1);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (src[i] === ")") return { items, end: i + 1 };
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function isTypeWrapperAt(src, i) {
|
|
870
|
+
return (
|
|
871
|
+
(src.startsWith("satisfies", i) && !/[\w$]/.test(src[i + 9] ?? "")) ||
|
|
872
|
+
(src.startsWith("as", i) && /[\s<]/.test(src[i + 2] ?? ""))
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Skips an annotation after satisfies/as. Generic argument commas are not
|
|
877
|
+
// argument separators, so `<...>` is tracked as depth.
|
|
878
|
+
function skipTypeExpression(src, start) {
|
|
879
|
+
let i = start;
|
|
880
|
+
let paren = 0;
|
|
881
|
+
let angle = 0;
|
|
882
|
+
while (i < src.length) {
|
|
883
|
+
const ch = src[i];
|
|
884
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
885
|
+
const parsed = parseString(src, i);
|
|
886
|
+
if (parsed) {
|
|
887
|
+
i = parsed.end;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (ch === "(" || ch === "[" || ch === "{") paren += 1;
|
|
892
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
893
|
+
if (paren === 0) return i;
|
|
894
|
+
paren -= 1;
|
|
895
|
+
} else if (ch === "<") angle += 1;
|
|
896
|
+
else if (ch === ">" && angle > 0) angle -= 1;
|
|
897
|
+
else if (ch === "," && paren === 0 && angle === 0) return i;
|
|
898
|
+
i += 1;
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// A handler argument (`async (args) => {...}`) is not a value we can parse; step
|
|
904
|
+
// over it so one unparseable parameter does not discard the whole definition.
|
|
905
|
+
function skipArgument(src, start) {
|
|
906
|
+
let i = start;
|
|
907
|
+
let depth = 0;
|
|
908
|
+
while (i < src.length) {
|
|
909
|
+
const ch = src[i];
|
|
910
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
911
|
+
const parsed = parseString(src, i);
|
|
912
|
+
if (parsed) {
|
|
913
|
+
i = parsed.end;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
918
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
919
|
+
if (depth === 0) return ch === ")" ? i : null;
|
|
920
|
+
depth -= 1;
|
|
921
|
+
} else if (ch === "," && depth === 0) return i;
|
|
922
|
+
i += 1;
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function readIdent(src, start) {
|
|
928
|
+
const match = /^[A-Za-z_$][\w$]*/.exec(src.slice(start, start + 120));
|
|
929
|
+
if (!match) return null;
|
|
930
|
+
return { name: match[0], end: start + match[0].length };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function skipTrivia(src, start) {
|
|
934
|
+
let i = start;
|
|
935
|
+
while (i < src.length) {
|
|
936
|
+
const ch = src[i];
|
|
937
|
+
if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
|
|
938
|
+
i += 1;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
942
|
+
const nl = src.indexOf("\n", i);
|
|
943
|
+
i = nl < 0 ? src.length : nl + 1;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
947
|
+
const close = src.indexOf("*/", i + 2);
|
|
948
|
+
i = close < 0 ? src.length : close + 2;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
return i;
|
|
952
|
+
}
|
|
953
|
+
return i;
|
|
954
|
+
}
|