@workos/oagen 0.1.1 → 0.3.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/cli/index.d.mts +2 -0
- package/dist/cli/index.mjs +6233 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/compat/{index.d.ts → index.d.mts} +43 -114
- package/dist/compat/index.d.mts.map +1 -0
- package/dist/compat/index.mjs +4562 -0
- package/dist/compat/index.mjs.map +1 -0
- package/dist/diff-Cn6S74vV.mjs +2432 -0
- package/dist/diff-Cn6S74vV.mjs.map +1 -0
- package/dist/differ-B6472Yef.mjs +1100 -0
- package/dist/differ-B6472Yef.mjs.map +1 -0
- package/dist/index.d.mts +324 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1553 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.d.mts +1 -0
- package/dist/scripts/smoke/baseline.mjs +1951 -0
- package/dist/scripts/smoke/baseline.mjs.map +1 -0
- package/dist/scripts/smoke/sdk-test.d.mts +1 -0
- package/dist/scripts/smoke/sdk-test.mjs +111 -0
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -0
- package/dist/smoke.d.mts +331 -0
- package/dist/smoke.d.mts.map +1 -0
- package/dist/smoke.mjs +2119 -0
- package/dist/smoke.mjs.map +1 -0
- package/dist/tree-sitter-DFRXfWg9.mjs +394 -0
- package/dist/tree-sitter-DFRXfWg9.mjs.map +1 -0
- package/dist/types-O25qZxMl.d.mts +397 -0
- package/dist/types-O25qZxMl.d.mts.map +1 -0
- package/dist/verify/index.d.mts +82 -0
- package/dist/verify/index.d.mts.map +1 -0
- package/dist/verify/index.mjs +327 -0
- package/dist/verify/index.mjs.map +1 -0
- package/package.json +19 -16
- package/dist/chunk-2JXFTDTO.js +0 -1669
- package/dist/chunk-CIX5NOBF.js +0 -263
- package/dist/chunk-IJ5E5AE7.js +0 -1040
- package/dist/cli/index.js +0 -5322
- package/dist/compat/index.js +0 -4491
- package/dist/index.d.ts +0 -277
- package/dist/index.js +0 -1565
- package/dist/scripts/smoke/baseline.js +0 -1813
- package/dist/scripts/smoke/sdk-test.js +0 -93
- package/dist/smoke.js +0 -2025
- package/dist/types-B5t9O9Wm.d.ts +0 -367
- package/dist/verify/index.d.ts +0 -76
- package/dist/verify/index.js +0 -336
|
@@ -0,0 +1,4562 @@
|
|
|
1
|
+
import { t as safeParse$1, v as ExtractorError } from "../tree-sitter-DFRXfWg9.mjs";
|
|
2
|
+
import { _ as registerExtractor, a as specDerivedHttpKeys, c as buildOverlayLookup, d as defaultIsNullableOnlyDifference, f as namedTypeWordsOverlap, g as getExtractor, h as typeExistsInSurface, i as specDerivedFieldPaths, l as patchOverlay, m as resolveHints, n as filterSurface, o as specDerivedMethodPaths, p as nodeHints, r as specDerivedEnumValues, s as specDerivedNames, t as diffSurfaces, u as NAMED_TYPE_RE } from "../differ-B6472Yef.mjs";
|
|
3
|
+
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import Parser from "tree-sitter";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import PhpGrammar from "tree-sitter-php";
|
|
8
|
+
import Python from "tree-sitter-python";
|
|
9
|
+
import Ruby from "tree-sitter-ruby";
|
|
10
|
+
import Go from "tree-sitter-go";
|
|
11
|
+
import RustLang from "tree-sitter-rust";
|
|
12
|
+
//#region src/compat/extractors/shared.ts
|
|
13
|
+
/** Sort an object's keys alphabetically. Shared across all extractors for deterministic output. */
|
|
14
|
+
function sortRecord(record) {
|
|
15
|
+
const sorted = {};
|
|
16
|
+
for (const key of Object.keys(record).sort()) sorted[key] = record[key];
|
|
17
|
+
return sorted;
|
|
18
|
+
}
|
|
19
|
+
/** Collects per-file export names, deduplicating and sorting on finalization. */
|
|
20
|
+
var ExportCollector = class {
|
|
21
|
+
map = /* @__PURE__ */ new Map();
|
|
22
|
+
add(sourceFile, name) {
|
|
23
|
+
let set = this.map.get(sourceFile);
|
|
24
|
+
if (!set) {
|
|
25
|
+
set = /* @__PURE__ */ new Set();
|
|
26
|
+
this.map.set(sourceFile, set);
|
|
27
|
+
}
|
|
28
|
+
set.add(name);
|
|
29
|
+
}
|
|
30
|
+
toRecord() {
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const [file, names] of this.map) result[file] = [...names].sort();
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/compat/extractors/node.ts
|
|
38
|
+
const nodeExtractor = {
|
|
39
|
+
language: "node",
|
|
40
|
+
hints: nodeHints,
|
|
41
|
+
async extract(sdkPath) {
|
|
42
|
+
sdkPath = resolve(sdkPath);
|
|
43
|
+
const configPath = ts.findConfigFile(sdkPath, ts.sys.fileExists, "tsconfig.json");
|
|
44
|
+
if (!configPath) throw new ExtractorError(`No tsconfig.json found in ${sdkPath}`, `Create a tsconfig.json in "${sdkPath}" or verify the --sdk-path argument points to a TypeScript project root.`);
|
|
45
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
46
|
+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, sdkPath);
|
|
47
|
+
const program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
|
|
48
|
+
const checker = program.getTypeChecker();
|
|
49
|
+
const entryPoint = resolveEntryPoint(sdkPath, program);
|
|
50
|
+
const entrySourceFile = program.getSourceFile(entryPoint);
|
|
51
|
+
if (!entrySourceFile) throw new ExtractorError(`Entry point not found: ${entryPoint}`, `Ensure the file "${entryPoint}" exists and is included in the tsconfig.json "include" or "files" array.`);
|
|
52
|
+
const moduleSymbol = checker.getSymbolAtLocation(entrySourceFile);
|
|
53
|
+
if (!moduleSymbol) throw new ExtractorError("No exports found at entry point", `Verify that "${entryPoint}" has at least one \`export\` statement. The extractor needs public exports to build an API surface.`);
|
|
54
|
+
const exportedSymbols = checker.getExportsOfModule(moduleSymbol);
|
|
55
|
+
const classes = {};
|
|
56
|
+
const interfaces = {};
|
|
57
|
+
const typeAliases = {};
|
|
58
|
+
const enums = {};
|
|
59
|
+
for (const sym of exportedSymbols) {
|
|
60
|
+
const resolved = resolveAlias(sym, checker);
|
|
61
|
+
const declarations = resolved.getDeclarations();
|
|
62
|
+
if (!declarations || declarations.length === 0) continue;
|
|
63
|
+
const decl = declarations[0];
|
|
64
|
+
const sourceFile = relative(sdkPath, decl.getSourceFile().fileName);
|
|
65
|
+
if (ts.isClassDeclaration(decl)) classes[resolved.name] = {
|
|
66
|
+
...extractClass(resolved, checker),
|
|
67
|
+
sourceFile
|
|
68
|
+
};
|
|
69
|
+
else if (ts.isInterfaceDeclaration(decl)) interfaces[resolved.name] = {
|
|
70
|
+
...extractInterface(resolved, checker),
|
|
71
|
+
sourceFile
|
|
72
|
+
};
|
|
73
|
+
else if (ts.isTypeAliasDeclaration(decl)) typeAliases[resolved.name] = {
|
|
74
|
+
...extractTypeAlias(resolved, checker),
|
|
75
|
+
sourceFile
|
|
76
|
+
};
|
|
77
|
+
else if (ts.isEnumDeclaration(decl)) enums[resolved.name] = {
|
|
78
|
+
...extractEnum(resolved, checker),
|
|
79
|
+
sourceFile
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
followPropertyTypeClasses(exportedSymbols, checker, classes, sdkPath);
|
|
83
|
+
const exports = buildExportMap(entrySourceFile, checker, sdkPath, program);
|
|
84
|
+
return {
|
|
85
|
+
language: "node",
|
|
86
|
+
extractedFrom: sdkPath,
|
|
87
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
classes: sortRecord(classes),
|
|
89
|
+
interfaces: sortRecord(interfaces),
|
|
90
|
+
typeAliases: sortRecord(typeAliases),
|
|
91
|
+
enums: sortRecord(enums),
|
|
92
|
+
exports: sortRecord(exports)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function followPropertyTypeClasses(exportedSymbols, checker, classes, sdkPath) {
|
|
97
|
+
const classSymbols = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const sym of exportedSymbols) {
|
|
99
|
+
const resolved = resolveAlias(sym, checker);
|
|
100
|
+
if (resolved.getDeclarations()?.some((d) => ts.isClassDeclaration(d))) classSymbols.set(resolved.name, resolved);
|
|
101
|
+
}
|
|
102
|
+
const toVisit = [...classSymbols.values()];
|
|
103
|
+
const visited = new Set(classSymbols.keys());
|
|
104
|
+
while (toVisit.length > 0) {
|
|
105
|
+
const sym = toVisit.pop();
|
|
106
|
+
const type = checker.getDeclaredTypeOfSymbol(sym);
|
|
107
|
+
const decls = sym.getDeclarations();
|
|
108
|
+
if (!decls || decls.length === 0) continue;
|
|
109
|
+
for (const prop of type.getProperties()) {
|
|
110
|
+
const propDecls = prop.getDeclarations();
|
|
111
|
+
if (!propDecls || propDecls.length === 0) continue;
|
|
112
|
+
const propDecl = propDecls[0];
|
|
113
|
+
if ((ts.canHaveModifiers(propDecl) ? ts.getModifiers(propDecl) : void 0)?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) continue;
|
|
114
|
+
const propSymbol = checker.getTypeOfSymbolAtLocation(prop, propDecl).getSymbol();
|
|
115
|
+
if (!propSymbol) continue;
|
|
116
|
+
const propName = propSymbol.name;
|
|
117
|
+
if (visited.has(propName)) continue;
|
|
118
|
+
const propSymDecls = propSymbol.getDeclarations();
|
|
119
|
+
if (!propSymDecls?.some((d) => ts.isClassDeclaration(d))) continue;
|
|
120
|
+
visited.add(propName);
|
|
121
|
+
const sourceFile = relative(sdkPath, propSymDecls[0].getSourceFile().fileName);
|
|
122
|
+
classes[propName] = {
|
|
123
|
+
...extractClass(propSymbol, checker),
|
|
124
|
+
sourceFile
|
|
125
|
+
};
|
|
126
|
+
toVisit.push(propSymbol);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function resolveEntryPoint(sdkPath, program) {
|
|
131
|
+
const pkgPath = resolve(sdkPath, "package.json");
|
|
132
|
+
try {
|
|
133
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
134
|
+
if (pkg.exports) {
|
|
135
|
+
const mainExport = typeof pkg.exports === "string" ? pkg.exports : pkg.exports["."]?.import ?? pkg.exports["."]?.default ?? pkg.exports["."];
|
|
136
|
+
if (typeof mainExport === "string") {
|
|
137
|
+
const resolved = resolveSourceFile(sdkPath, mainExport, program);
|
|
138
|
+
if (resolved) return resolved;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (pkg.main) {
|
|
142
|
+
const resolved = resolveSourceFile(sdkPath, pkg.main, program);
|
|
143
|
+
if (resolved) return resolved;
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
const fallback = resolve(sdkPath, "src/index.ts");
|
|
147
|
+
if (program.getSourceFile(fallback)) return fallback;
|
|
148
|
+
throw new ExtractorError(`No entry point found. Tried: package.json exports/main, src/index.ts`, `Add an "exports" or "main" field to package.json in "${sdkPath}", or create a src/index.ts file that re-exports the public API.`);
|
|
149
|
+
}
|
|
150
|
+
function resolveSourceFile(sdkPath, entryPath, program) {
|
|
151
|
+
const candidates = [
|
|
152
|
+
resolve(sdkPath, entryPath),
|
|
153
|
+
resolve(sdkPath, entryPath.replace(/\.js$/, ".ts")),
|
|
154
|
+
resolve(sdkPath, entryPath.replace(/\.js$/, "/index.ts"))
|
|
155
|
+
];
|
|
156
|
+
for (const candidate of candidates) if (program.getSourceFile(candidate)) return candidate;
|
|
157
|
+
}
|
|
158
|
+
function resolveAlias(sym, checker) {
|
|
159
|
+
if (sym.flags & ts.SymbolFlags.Alias) return checker.getAliasedSymbol(sym);
|
|
160
|
+
return sym;
|
|
161
|
+
}
|
|
162
|
+
function extractClass(sym, checker) {
|
|
163
|
+
const type = checker.getDeclaredTypeOfSymbol(sym);
|
|
164
|
+
const methods = {};
|
|
165
|
+
const properties = {};
|
|
166
|
+
const constructorParams = [];
|
|
167
|
+
const declarations = sym.getDeclarations();
|
|
168
|
+
if (declarations) {
|
|
169
|
+
for (const decl of declarations) if (ts.isClassDeclaration(decl)) {
|
|
170
|
+
for (const member of decl.members) if (ts.isConstructorDeclaration(member)) for (const param of member.parameters) {
|
|
171
|
+
const paramName = param.name.getText();
|
|
172
|
+
const paramType = param.type ? checker.typeToString(checker.getTypeFromTypeNode(param.type)) : "any";
|
|
173
|
+
constructorParams.push({
|
|
174
|
+
name: paramName,
|
|
175
|
+
type: paramType,
|
|
176
|
+
optional: !!param.questionToken || !!param.initializer
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const prop of type.getProperties()) {
|
|
182
|
+
const propDeclarations = prop.getDeclarations();
|
|
183
|
+
if (propDeclarations && propDeclarations.length > 0) {
|
|
184
|
+
const decl = propDeclarations[0];
|
|
185
|
+
if ((ts.canHaveModifiers(decl) ? ts.getModifiers(decl) : void 0)?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword)) continue;
|
|
186
|
+
}
|
|
187
|
+
const propType = checker.getTypeOfSymbolAtLocation(prop, declarations[0]);
|
|
188
|
+
const callSignatures = propType.getCallSignatures();
|
|
189
|
+
if (callSignatures.length > 0) {
|
|
190
|
+
if (!methods[prop.name]) methods[prop.name] = [];
|
|
191
|
+
for (const sig of callSignatures) methods[prop.name].push({
|
|
192
|
+
name: prop.name,
|
|
193
|
+
params: sig.getParameters().map((p) => extractParam(p, checker)),
|
|
194
|
+
returnType: checker.typeToString(sig.getReturnType()),
|
|
195
|
+
async: checker.typeToString(sig.getReturnType()).startsWith("Promise<")
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
const typeStr = checker.typeToString(propType);
|
|
199
|
+
const isReadonly = propDeclarations?.some((d) => {
|
|
200
|
+
return (ts.canHaveModifiers(d) ? ts.getModifiers(d) : void 0)?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword);
|
|
201
|
+
});
|
|
202
|
+
properties[prop.name] = {
|
|
203
|
+
name: prop.name,
|
|
204
|
+
type: typeStr,
|
|
205
|
+
readonly: !!isReadonly
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
name: sym.name,
|
|
211
|
+
methods: sortRecord(methods),
|
|
212
|
+
properties: sortRecord(properties),
|
|
213
|
+
constructorParams
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function extractParam(sym, checker) {
|
|
217
|
+
const decl = sym.getDeclarations()?.[0];
|
|
218
|
+
const isOptional = decl && ts.isParameter(decl) ? !!decl.questionToken || !!decl.initializer : false;
|
|
219
|
+
const type = checker.getTypeOfSymbolAtLocation(sym, decl);
|
|
220
|
+
return {
|
|
221
|
+
name: sym.name,
|
|
222
|
+
type: checker.typeToString(type),
|
|
223
|
+
optional: isOptional
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function extractInterface(sym, checker) {
|
|
227
|
+
const type = checker.getDeclaredTypeOfSymbol(sym);
|
|
228
|
+
const fields = {};
|
|
229
|
+
const extendsNames = [];
|
|
230
|
+
const declarations = sym.getDeclarations();
|
|
231
|
+
if (declarations) {
|
|
232
|
+
for (const decl of declarations) if (ts.isInterfaceDeclaration(decl) && decl.heritageClauses) for (const clause of decl.heritageClauses) for (const typeExpr of clause.types) extendsNames.push(typeExpr.getText());
|
|
233
|
+
}
|
|
234
|
+
for (const prop of type.getProperties()) {
|
|
235
|
+
const propType = checker.getTypeOfSymbolAtLocation(prop, declarations[0]);
|
|
236
|
+
const propDecl = prop.getDeclarations()?.[0];
|
|
237
|
+
const isOptional = propDecl && ts.isPropertySignature(propDecl) ? !!propDecl.questionToken : false;
|
|
238
|
+
let typeStr = checker.typeToString(propType);
|
|
239
|
+
if (isOptional) typeStr = stripUndefined(typeStr);
|
|
240
|
+
fields[prop.name] = {
|
|
241
|
+
name: prop.name,
|
|
242
|
+
type: typeStr,
|
|
243
|
+
optional: isOptional
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (declarations) {
|
|
247
|
+
for (const decl of declarations) if (ts.isInterfaceDeclaration(decl)) {
|
|
248
|
+
for (const member of decl.members) if (ts.isPropertySignature(member) && member.name) {
|
|
249
|
+
const memberName = member.name.getText();
|
|
250
|
+
if (!fields[memberName]) {
|
|
251
|
+
const memberType = member.type ? checker.typeToString(checker.getTypeFromTypeNode(member.type)) : "any";
|
|
252
|
+
const isOpt = !!member.questionToken;
|
|
253
|
+
fields[memberName] = {
|
|
254
|
+
name: memberName,
|
|
255
|
+
type: isOpt ? stripUndefined(memberType) : memberType,
|
|
256
|
+
optional: isOpt
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
name: sym.name,
|
|
264
|
+
fields: sortRecord(fields),
|
|
265
|
+
extends: extendsNames.sort()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function extractTypeAlias(sym, checker) {
|
|
269
|
+
const type = checker.getDeclaredTypeOfSymbol(sym);
|
|
270
|
+
return {
|
|
271
|
+
name: sym.name,
|
|
272
|
+
value: checker.typeToString(type, void 0, ts.TypeFormatFlags.InTypeAlias | ts.TypeFormatFlags.NoTruncation)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function extractEnum(sym, checker) {
|
|
276
|
+
const members = {};
|
|
277
|
+
const type = checker.getDeclaredTypeOfSymbol(sym);
|
|
278
|
+
if (type.isUnion()) {
|
|
279
|
+
for (const memberType of type.types) if (memberType.symbol) {
|
|
280
|
+
const constValue = checker.getConstantValue(memberType.symbol.getDeclarations()[0]);
|
|
281
|
+
if (constValue !== void 0) members[memberType.symbol.name] = constValue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
name: sym.name,
|
|
286
|
+
members: sortRecord(members)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function buildExportMap(entryFile, checker, sdkPath, program) {
|
|
290
|
+
const exports = {};
|
|
291
|
+
const visited = /* @__PURE__ */ new Set();
|
|
292
|
+
function walk(sourceFile) {
|
|
293
|
+
const absPath = sourceFile.fileName;
|
|
294
|
+
if (visited.has(absPath)) return;
|
|
295
|
+
visited.add(absPath);
|
|
296
|
+
const relPath = relative(sdkPath, absPath);
|
|
297
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
298
|
+
if (moduleSymbol) {
|
|
299
|
+
const names = checker.getExportsOfModule(moduleSymbol).map((s) => s.name);
|
|
300
|
+
if (names.length > 0) exports[relPath] = names.sort();
|
|
301
|
+
}
|
|
302
|
+
for (const stmt of sourceFile.statements) if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
|
|
303
|
+
const target = stmt.moduleSpecifier.text;
|
|
304
|
+
const resolved = resolveSourceFile(path.dirname(absPath), target, program);
|
|
305
|
+
if (resolved) {
|
|
306
|
+
const targetFile = program.getSourceFile(resolved);
|
|
307
|
+
if (targetFile) walk(targetFile);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
walk(entryFile);
|
|
312
|
+
return exports;
|
|
313
|
+
}
|
|
314
|
+
function stripUndefined(typeStr) {
|
|
315
|
+
return typeStr.replace(/\s*\|\s*undefined$/, "").replace(/^undefined\s*\|\s*/, "");
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/compat/extractors/php-parser.ts
|
|
319
|
+
/**
|
|
320
|
+
* PHP source parser — tree-sitter-based extraction of classes, interfaces,
|
|
321
|
+
* methods, properties, and constants from PHP source files.
|
|
322
|
+
*/
|
|
323
|
+
const phpParser = new Parser();
|
|
324
|
+
phpParser.setLanguage(PhpGrammar.php_only);
|
|
325
|
+
function walkPhpFiles(dir) {
|
|
326
|
+
const results = [];
|
|
327
|
+
let entries;
|
|
328
|
+
try {
|
|
329
|
+
entries = readdirSync(dir);
|
|
330
|
+
} catch {
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
const fullPath = join(dir, entry);
|
|
335
|
+
let stat;
|
|
336
|
+
try {
|
|
337
|
+
stat = statSync(fullPath);
|
|
338
|
+
} catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (stat.isDirectory()) {
|
|
342
|
+
if (entry.startsWith(".") || entry === "vendor" || entry === "tests" || entry === "test") continue;
|
|
343
|
+
results.push(...walkPhpFiles(fullPath));
|
|
344
|
+
} else if (entry.endsWith(".php")) results.push(fullPath);
|
|
345
|
+
}
|
|
346
|
+
return results.sort();
|
|
347
|
+
}
|
|
348
|
+
function parsePhpDoc(commentText) {
|
|
349
|
+
const params = /* @__PURE__ */ new Map();
|
|
350
|
+
let returnType = "";
|
|
351
|
+
const lines = commentText.split("\n");
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
const paramMatch = line.match(/@param\s+(\S+)\s+\$(\w+)/);
|
|
354
|
+
if (paramMatch) params.set(paramMatch[2], paramMatch[1]);
|
|
355
|
+
const returnMatch = line.match(/@return\s+(\S+)/);
|
|
356
|
+
if (returnMatch) returnType = returnMatch[1];
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
params,
|
|
360
|
+
returnType
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** Find the doc comment immediately preceding a node. */
|
|
364
|
+
function findDocComment(node) {
|
|
365
|
+
let sibling = node.previousNamedSibling;
|
|
366
|
+
if (sibling && sibling.type === "comment" && sibling.text.startsWith("/**")) return parsePhpDoc(sibling.text);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
function getVisibility(node) {
|
|
370
|
+
for (const child of node.children) if (child.type === "visibility_modifier") {
|
|
371
|
+
const text = child.text.toLowerCase();
|
|
372
|
+
if (text === "public") return "public";
|
|
373
|
+
if (text === "protected") return "protected";
|
|
374
|
+
if (text === "private") return "private";
|
|
375
|
+
}
|
|
376
|
+
return "public";
|
|
377
|
+
}
|
|
378
|
+
function isStaticMethod(node) {
|
|
379
|
+
for (const child of node.children) if (child.type === "static_modifier") return true;
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
/** Parse a PHP array literal to extract string values: ["id", "name", ...] */
|
|
383
|
+
function parseArrayLiteral(node) {
|
|
384
|
+
const values = [];
|
|
385
|
+
for (const elemInit of node.descendantsOfType("array_element_initializer")) {
|
|
386
|
+
const strings = elemInit.namedChildren.filter((c) => c.type === "encapsed_string" || c.type === "string");
|
|
387
|
+
if (strings.length === 1) {
|
|
388
|
+
const contentNode = strings[0].namedChildren.find((c) => c.type === "string_content");
|
|
389
|
+
if (contentNode) values.push(contentNode.text);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return values;
|
|
393
|
+
}
|
|
394
|
+
/** Extract the name and value from a const_element node. */
|
|
395
|
+
function parseConstElement(constElement) {
|
|
396
|
+
const nameNode = constElement.namedChildren.find((c) => c.type === "name");
|
|
397
|
+
if (!nameNode) return null;
|
|
398
|
+
const constName = nameNode.text;
|
|
399
|
+
if (constName === "RESOURCE_ATTRIBUTES" || constName === "RESPONSE_TO_RESOURCE_KEY") return null;
|
|
400
|
+
const valueNode = constElement.namedChildren.find((c) => c.type !== "name");
|
|
401
|
+
if (!valueNode) return null;
|
|
402
|
+
if (valueNode.type === "encapsed_string") {
|
|
403
|
+
const contentNode = valueNode.namedChildren.find((c) => c.type === "string_content");
|
|
404
|
+
return {
|
|
405
|
+
name: constName,
|
|
406
|
+
value: contentNode ? contentNode.text : ""
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (valueNode.type === "string") {
|
|
410
|
+
const contentNode = valueNode.namedChildren.find((c) => c.type === "string_content");
|
|
411
|
+
return {
|
|
412
|
+
name: constName,
|
|
413
|
+
value: contentNode ? contentNode.text : ""
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
name: constName,
|
|
418
|
+
value: valueNode.text
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function parseMethods$2(classBody) {
|
|
422
|
+
const methods = [];
|
|
423
|
+
for (const child of classBody.namedChildren) {
|
|
424
|
+
if (child.type !== "method_declaration") continue;
|
|
425
|
+
const nameNode = child.childForFieldName("name");
|
|
426
|
+
if (!nameNode) continue;
|
|
427
|
+
const methodName = nameNode.text;
|
|
428
|
+
if (methodName.startsWith("__") && methodName !== "__construct") continue;
|
|
429
|
+
const visibility = getVisibility(child);
|
|
430
|
+
const isStatic = isStaticMethod(child);
|
|
431
|
+
const docInfo = findDocComment(child);
|
|
432
|
+
const params = [];
|
|
433
|
+
const paramsList = child.childForFieldName("parameters");
|
|
434
|
+
if (paramsList) for (const paramNode of paramsList.namedChildren) {
|
|
435
|
+
if (paramNode.type !== "simple_parameter") continue;
|
|
436
|
+
const paramNameNode = paramNode.namedChildren.find((c) => c.type === "variable_name");
|
|
437
|
+
if (!paramNameNode) continue;
|
|
438
|
+
const paramName = paramNameNode.text.replace(/^\$/, "");
|
|
439
|
+
let paramType = "";
|
|
440
|
+
const typeNode = paramNode.childForFieldName("type");
|
|
441
|
+
if (typeNode) paramType = typeNode.text;
|
|
442
|
+
if (!paramType && docInfo) paramType = docInfo.params.get(paramName) || "mixed";
|
|
443
|
+
if (!paramType) paramType = "mixed";
|
|
444
|
+
const hasDefault = paramNode.childForFieldName("default_value") !== null;
|
|
445
|
+
params.push({
|
|
446
|
+
name: paramName,
|
|
447
|
+
type: paramType,
|
|
448
|
+
optional: hasDefault
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
let returnType = "";
|
|
452
|
+
const returnTypeNode = child.childForFieldName("return_type");
|
|
453
|
+
if (returnTypeNode) returnType = returnTypeNode.text;
|
|
454
|
+
if (!returnType && docInfo) returnType = docInfo.returnType || "mixed";
|
|
455
|
+
if (!returnType) returnType = "mixed";
|
|
456
|
+
methods.push({
|
|
457
|
+
name: methodName,
|
|
458
|
+
visibility,
|
|
459
|
+
isStatic,
|
|
460
|
+
params,
|
|
461
|
+
returnType
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return methods;
|
|
465
|
+
}
|
|
466
|
+
function parseProperties$1(classBody) {
|
|
467
|
+
const properties = [];
|
|
468
|
+
for (const child of classBody.namedChildren) {
|
|
469
|
+
if (child.type !== "property_declaration") continue;
|
|
470
|
+
const visibility = getVisibility(child);
|
|
471
|
+
for (const propElement of child.namedChildren) {
|
|
472
|
+
if (propElement.type !== "property_element") continue;
|
|
473
|
+
const varNameNode = propElement.namedChildren.find((c) => c.type === "variable_name");
|
|
474
|
+
if (!varNameNode) continue;
|
|
475
|
+
const propName = varNameNode.text.replace(/^\$/, "");
|
|
476
|
+
let propType = "mixed";
|
|
477
|
+
const typeNode = child.namedChildren.find((c) => c.type === "union_type" || c.type === "named_type" || c.type === "optional_type" || c.type === "primitive_type");
|
|
478
|
+
if (typeNode) propType = typeNode.text;
|
|
479
|
+
properties.push({
|
|
480
|
+
name: propName,
|
|
481
|
+
type: propType,
|
|
482
|
+
visibility
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return properties;
|
|
487
|
+
}
|
|
488
|
+
function parseConstants(classBody) {
|
|
489
|
+
const constants = [];
|
|
490
|
+
for (const child of classBody.namedChildren) {
|
|
491
|
+
if (child.type !== "const_declaration") continue;
|
|
492
|
+
for (const constElement of child.namedChildren) {
|
|
493
|
+
if (constElement.type !== "const_element") continue;
|
|
494
|
+
const parsed = parseConstElement(constElement);
|
|
495
|
+
if (parsed) constants.push(parsed);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return constants;
|
|
499
|
+
}
|
|
500
|
+
function parseResourceAttributes(classBody) {
|
|
501
|
+
for (const child of classBody.namedChildren) {
|
|
502
|
+
if (child.type !== "const_declaration") continue;
|
|
503
|
+
for (const constElement of child.namedChildren) {
|
|
504
|
+
if (constElement.type !== "const_element") continue;
|
|
505
|
+
const nameNode = constElement.namedChildren.find((c) => c.type === "name");
|
|
506
|
+
if (!nameNode || nameNode.text !== "RESOURCE_ATTRIBUTES") continue;
|
|
507
|
+
const arrayNode = constElement.namedChildren.find((c) => c.type === "array_creation_expression");
|
|
508
|
+
if (!arrayNode) continue;
|
|
509
|
+
return parseArrayLiteral(arrayNode);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
function extractNamespace$1(tree) {
|
|
515
|
+
for (const node of tree.rootNode.namedChildren) if (node.type === "namespace_definition") {
|
|
516
|
+
const nameNode = node.childForFieldName("name");
|
|
517
|
+
return nameNode ? nameNode.text : "";
|
|
518
|
+
}
|
|
519
|
+
return "";
|
|
520
|
+
}
|
|
521
|
+
function parseClassDeclarations(tree, sourceFile, namespace) {
|
|
522
|
+
const classes = [];
|
|
523
|
+
for (const node of tree.rootNode.descendantsOfType("class_declaration")) {
|
|
524
|
+
const nameNode = node.childForFieldName("name");
|
|
525
|
+
if (!nameNode) continue;
|
|
526
|
+
let extendsName = null;
|
|
527
|
+
const baseClause = node.namedChildren.find((c) => c.type === "base_clause");
|
|
528
|
+
if (baseClause) {
|
|
529
|
+
const baseNameNode = baseClause.namedChildren.find((c) => c.type === "name" || c.type === "qualified_name");
|
|
530
|
+
if (baseNameNode) {
|
|
531
|
+
const parts = baseNameNode.text.replace(/^\\/, "").split("\\");
|
|
532
|
+
extendsName = parts[parts.length - 1];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const bodyNode = node.childForFieldName("body");
|
|
536
|
+
if (!bodyNode) continue;
|
|
537
|
+
const methods = parseMethods$2(bodyNode);
|
|
538
|
+
const properties = parseProperties$1(bodyNode);
|
|
539
|
+
const constants = parseConstants(bodyNode);
|
|
540
|
+
const resourceAttributes = parseResourceAttributes(bodyNode);
|
|
541
|
+
classes.push({
|
|
542
|
+
name: nameNode.text,
|
|
543
|
+
namespace,
|
|
544
|
+
extends: extendsName,
|
|
545
|
+
isInterface: false,
|
|
546
|
+
methods,
|
|
547
|
+
properties,
|
|
548
|
+
constants,
|
|
549
|
+
resourceAttributes,
|
|
550
|
+
sourceFile
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return classes;
|
|
554
|
+
}
|
|
555
|
+
function parseInterfaceDeclarations(tree, sourceFile, namespace) {
|
|
556
|
+
const interfaces = [];
|
|
557
|
+
for (const node of tree.rootNode.descendantsOfType("interface_declaration")) {
|
|
558
|
+
const nameNode = node.childForFieldName("name");
|
|
559
|
+
if (!nameNode) continue;
|
|
560
|
+
const bodyNode = node.childForFieldName("body");
|
|
561
|
+
if (!bodyNode) continue;
|
|
562
|
+
const methods = parseMethods$2(bodyNode);
|
|
563
|
+
interfaces.push({
|
|
564
|
+
name: nameNode.text,
|
|
565
|
+
namespace,
|
|
566
|
+
extends: null,
|
|
567
|
+
isInterface: true,
|
|
568
|
+
methods,
|
|
569
|
+
properties: [],
|
|
570
|
+
constants: [],
|
|
571
|
+
resourceAttributes: [],
|
|
572
|
+
sourceFile
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return interfaces;
|
|
576
|
+
}
|
|
577
|
+
function parseEnumDeclarations(tree, sourceFile, namespace) {
|
|
578
|
+
const enums = [];
|
|
579
|
+
for (const node of tree.rootNode.descendantsOfType("enum_declaration")) {
|
|
580
|
+
const nameNode = node.childForFieldName("name");
|
|
581
|
+
if (!nameNode) continue;
|
|
582
|
+
const bodyNode = node.childForFieldName("body");
|
|
583
|
+
if (!bodyNode) continue;
|
|
584
|
+
const constants = [];
|
|
585
|
+
for (const child of bodyNode.namedChildren) {
|
|
586
|
+
if (child.type !== "enum_case") continue;
|
|
587
|
+
const caseNameNode = child.childForFieldName("name");
|
|
588
|
+
if (!caseNameNode) continue;
|
|
589
|
+
let value = caseNameNode.text;
|
|
590
|
+
const valueNode = child.namedChildren.find((c) => c.type === "encapsed_string" || c.type === "string" || c.type === "integer");
|
|
591
|
+
if (valueNode) {
|
|
592
|
+
const contentNode = valueNode.namedChildren.find((c) => c.type === "string_content");
|
|
593
|
+
value = contentNode ? contentNode.text : valueNode.text;
|
|
594
|
+
}
|
|
595
|
+
constants.push({
|
|
596
|
+
name: caseNameNode.text,
|
|
597
|
+
value
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const methods = parseMethods$2(bodyNode);
|
|
601
|
+
enums.push({
|
|
602
|
+
name: nameNode.text,
|
|
603
|
+
namespace,
|
|
604
|
+
extends: null,
|
|
605
|
+
isInterface: false,
|
|
606
|
+
methods,
|
|
607
|
+
properties: [],
|
|
608
|
+
constants,
|
|
609
|
+
resourceAttributes: [],
|
|
610
|
+
sourceFile
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return enums;
|
|
614
|
+
}
|
|
615
|
+
/** Parse a single PHP source file and return all extracted symbols. */
|
|
616
|
+
function parsePhpFile(filePath, sdkPath) {
|
|
617
|
+
const source = readFileSync(filePath, "utf-8");
|
|
618
|
+
const relPath = relative(sdkPath, filePath);
|
|
619
|
+
const tree = safeParse$1(phpParser, source);
|
|
620
|
+
const namespace = extractNamespace$1(tree);
|
|
621
|
+
const classDecls = parseClassDeclarations(tree, relPath, namespace);
|
|
622
|
+
const interfaceDecls = parseInterfaceDeclarations(tree, relPath, namespace);
|
|
623
|
+
const enumDecls = parseEnumDeclarations(tree, relPath, namespace);
|
|
624
|
+
return { classes: [
|
|
625
|
+
...classDecls,
|
|
626
|
+
...interfaceDecls,
|
|
627
|
+
...enumDecls
|
|
628
|
+
] };
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/compat/extractors/php-surface.ts
|
|
632
|
+
/** Default resource base classes (empty — consumer provides via hints.modelBaseClasses). */
|
|
633
|
+
const DEFAULT_RESOURCE_BASES = [];
|
|
634
|
+
/** Default exception base classes. */
|
|
635
|
+
const DEFAULT_EXCEPTION_BASES$1 = ["Exception", "\\Exception"];
|
|
636
|
+
/** Check if a class is a resource class (extends a known resource base and has attributes). */
|
|
637
|
+
function isResourceClass(cls, resourceBases) {
|
|
638
|
+
return !!cls.extends && resourceBases.has(cls.extends) && cls.resourceAttributes.length > 0;
|
|
639
|
+
}
|
|
640
|
+
/** Check if a class is enum-like (only constants, no public methods). */
|
|
641
|
+
function isEnumClass(cls) {
|
|
642
|
+
if (cls.isInterface) return false;
|
|
643
|
+
if (cls.constants.length === 0) return false;
|
|
644
|
+
return cls.methods.filter((m) => m.visibility === "public" && m.name !== "__construct").length === 0 && cls.properties.length === 0 && !cls.extends;
|
|
645
|
+
}
|
|
646
|
+
/** Check if a class is an exception. */
|
|
647
|
+
function isExceptionClass$1(cls, exceptionBases) {
|
|
648
|
+
if (!cls.extends) return false;
|
|
649
|
+
return exceptionBases.has(cls.extends);
|
|
650
|
+
}
|
|
651
|
+
function buildSurface$6(allClasses, hints) {
|
|
652
|
+
const resourceBases = new Set(hints?.modelBaseClasses ?? DEFAULT_RESOURCE_BASES);
|
|
653
|
+
const exceptionBases = new Set(hints?.exceptionBaseClasses ?? DEFAULT_EXCEPTION_BASES$1);
|
|
654
|
+
const classes = {};
|
|
655
|
+
const interfaces = {};
|
|
656
|
+
const enums = {};
|
|
657
|
+
const collector = new ExportCollector();
|
|
658
|
+
for (const cls of allClasses) if (cls.isInterface) {
|
|
659
|
+
const fields = {};
|
|
660
|
+
for (const method of cls.methods) {
|
|
661
|
+
if (method.visibility !== "public") continue;
|
|
662
|
+
fields[method.name] = {
|
|
663
|
+
name: method.name,
|
|
664
|
+
type: method.returnType || "mixed",
|
|
665
|
+
optional: false
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
interfaces[cls.name] = {
|
|
669
|
+
name: cls.name,
|
|
670
|
+
sourceFile: cls.sourceFile,
|
|
671
|
+
fields: sortRecord(fields),
|
|
672
|
+
extends: []
|
|
673
|
+
};
|
|
674
|
+
collector.add(cls.sourceFile, cls.name);
|
|
675
|
+
} else if (isResourceClass(cls, resourceBases)) {
|
|
676
|
+
const fields = {};
|
|
677
|
+
for (const attr of cls.resourceAttributes) fields[attr] = {
|
|
678
|
+
name: attr,
|
|
679
|
+
type: "mixed",
|
|
680
|
+
optional: false
|
|
681
|
+
};
|
|
682
|
+
interfaces[cls.name] = {
|
|
683
|
+
name: cls.name,
|
|
684
|
+
sourceFile: cls.sourceFile,
|
|
685
|
+
fields: sortRecord(fields),
|
|
686
|
+
extends: cls.extends ? [cls.extends] : []
|
|
687
|
+
};
|
|
688
|
+
collector.add(cls.sourceFile, cls.name);
|
|
689
|
+
} else if (isEnumClass(cls)) {
|
|
690
|
+
const members = {};
|
|
691
|
+
for (const constant of cls.constants) members[constant.name] = constant.value;
|
|
692
|
+
enums[cls.name] = {
|
|
693
|
+
name: cls.name,
|
|
694
|
+
sourceFile: cls.sourceFile,
|
|
695
|
+
members: sortRecord(members)
|
|
696
|
+
};
|
|
697
|
+
collector.add(cls.sourceFile, cls.name);
|
|
698
|
+
} else {
|
|
699
|
+
const apiMethods = {};
|
|
700
|
+
const properties = {};
|
|
701
|
+
for (const method of cls.methods) {
|
|
702
|
+
if (method.visibility !== "public") continue;
|
|
703
|
+
const params = method.params.map((p) => ({
|
|
704
|
+
name: p.name,
|
|
705
|
+
type: p.type,
|
|
706
|
+
optional: p.optional
|
|
707
|
+
}));
|
|
708
|
+
if (!apiMethods[method.name]) apiMethods[method.name] = [];
|
|
709
|
+
apiMethods[method.name].push({
|
|
710
|
+
name: method.name,
|
|
711
|
+
params,
|
|
712
|
+
returnType: method.returnType || "mixed",
|
|
713
|
+
async: false
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
for (const prop of cls.properties) {
|
|
717
|
+
if (prop.visibility !== "public") continue;
|
|
718
|
+
properties[prop.name] = {
|
|
719
|
+
name: prop.name,
|
|
720
|
+
type: prop.type,
|
|
721
|
+
readonly: false
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (Object.keys(apiMethods).length > 0 || Object.keys(properties).length > 0 || isExceptionClass$1(cls, exceptionBases)) {
|
|
725
|
+
classes[cls.name] = {
|
|
726
|
+
name: cls.name,
|
|
727
|
+
sourceFile: cls.sourceFile,
|
|
728
|
+
methods: sortRecord(apiMethods),
|
|
729
|
+
properties: sortRecord(properties),
|
|
730
|
+
constructorParams: []
|
|
731
|
+
};
|
|
732
|
+
collector.add(cls.sourceFile, cls.name);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const exports = collector.toRecord();
|
|
736
|
+
return {
|
|
737
|
+
classes: sortRecord(classes),
|
|
738
|
+
interfaces: sortRecord(interfaces),
|
|
739
|
+
enums: sortRecord(enums),
|
|
740
|
+
exports: sortRecord(exports)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/compat/extractors/php.ts
|
|
745
|
+
/**
|
|
746
|
+
* PHP API surface extractor.
|
|
747
|
+
*
|
|
748
|
+
* Walks `.php` files under `lib/` (or `src/`), parses classes, interfaces,
|
|
749
|
+
* methods, properties, and constants, then builds an `ApiSurface`.
|
|
750
|
+
*
|
|
751
|
+
* Parser: `php-parser.ts` — tree-sitter-based PHP source analysis
|
|
752
|
+
* Surface: `php-surface.ts` — ApiSurface construction
|
|
753
|
+
*/
|
|
754
|
+
const PHP_BODY_TYPES = new Set(["array", "mixed"]);
|
|
755
|
+
const phpHints = {
|
|
756
|
+
stripNullable(type) {
|
|
757
|
+
if (type.startsWith("?")) return type.slice(1);
|
|
758
|
+
const parts = type.split("|").filter((p) => p.trim().toLowerCase() !== "null");
|
|
759
|
+
if (parts.length < type.split("|").length) return parts.join("|");
|
|
760
|
+
return null;
|
|
761
|
+
},
|
|
762
|
+
isNullableOnlyDifference(a, b) {
|
|
763
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
764
|
+
},
|
|
765
|
+
isUnionReorder(a, b) {
|
|
766
|
+
const sortParts = (t) => t.split("|").map((s) => s.trim()).sort().join("|");
|
|
767
|
+
return a !== b && sortParts(a) === sortParts(b);
|
|
768
|
+
},
|
|
769
|
+
isGenericTypeParam(_type) {
|
|
770
|
+
return false;
|
|
771
|
+
},
|
|
772
|
+
isExtractionArtifact(type) {
|
|
773
|
+
return type === "mixed" || type === "Object";
|
|
774
|
+
},
|
|
775
|
+
tolerateCategoryMismatch: false,
|
|
776
|
+
extractReturnTypeName(returnType) {
|
|
777
|
+
const stripped = this.stripNullable(returnType) ?? returnType;
|
|
778
|
+
if ([
|
|
779
|
+
"string",
|
|
780
|
+
"int",
|
|
781
|
+
"float",
|
|
782
|
+
"bool",
|
|
783
|
+
"void",
|
|
784
|
+
"array",
|
|
785
|
+
"mixed"
|
|
786
|
+
].includes(stripped)) return null;
|
|
787
|
+
return stripped.replace(/^\\/, "");
|
|
788
|
+
},
|
|
789
|
+
extractParamTypeName(paramType) {
|
|
790
|
+
const stripped = this.stripNullable(paramType) ?? paramType;
|
|
791
|
+
if ([
|
|
792
|
+
"string",
|
|
793
|
+
"int",
|
|
794
|
+
"float",
|
|
795
|
+
"bool",
|
|
796
|
+
"void",
|
|
797
|
+
"array",
|
|
798
|
+
"mixed",
|
|
799
|
+
"null",
|
|
800
|
+
"callable"
|
|
801
|
+
].includes(stripped)) return null;
|
|
802
|
+
return stripped.replace(/^\\/, "");
|
|
803
|
+
},
|
|
804
|
+
propertyMatchesClass(propertyName, className) {
|
|
805
|
+
return propertyName.toLowerCase() === className.toLowerCase();
|
|
806
|
+
},
|
|
807
|
+
derivedModelNames(_modelName) {
|
|
808
|
+
return [];
|
|
809
|
+
},
|
|
810
|
+
isSignatureEquivalent(baseline, candidate, _candidateSurface) {
|
|
811
|
+
if (!(baseline.returnType === candidate.returnType || baseline.returnType === "mixed" || candidate.returnType === "mixed" || baseline.returnType === "void" && candidate.returnType === "void" || baseline.returnType === "array" && candidate.returnType === "array" || baseline.returnType.includes("\\Resource\\") || candidate.returnType.includes("\\Resource\\"))) return false;
|
|
812
|
+
if (baseline.params.length === candidate.params.length) {
|
|
813
|
+
let allTypesMatch = true;
|
|
814
|
+
for (let i = 0; i < baseline.params.length; i++) if (baseline.params[i].type !== candidate.params[i].type) {
|
|
815
|
+
allTypesMatch = false;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
if (allTypesMatch) return true;
|
|
819
|
+
}
|
|
820
|
+
if (candidate.params.some((p) => PHP_BODY_TYPES.has(p.type))) return true;
|
|
821
|
+
if (baseline.params.length >= 1 && candidate.params.length >= 1) {
|
|
822
|
+
let typeMatch = true;
|
|
823
|
+
const minLen = Math.min(baseline.params.length, candidate.params.length);
|
|
824
|
+
for (let i = 0; i < minLen; i++) if (baseline.params[i].type !== candidate.params[i].type) {
|
|
825
|
+
typeMatch = false;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
if (typeMatch) {
|
|
829
|
+
const extraCandOk = candidate.params.slice(minLen).every((p) => p.optional);
|
|
830
|
+
const extraBaseOk = baseline.params.slice(minLen).every((p) => p.optional);
|
|
831
|
+
if (extraCandOk && extraBaseOk) return true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return false;
|
|
835
|
+
},
|
|
836
|
+
modelBaseClasses: ["BaseWorkOSResource"],
|
|
837
|
+
exceptionBaseClasses: ["Exception", "\\Exception"]
|
|
838
|
+
};
|
|
839
|
+
/** Create a PHP extractor with optional hint overrides. */
|
|
840
|
+
function createPhpExtractor(hintOverrides) {
|
|
841
|
+
const mergedHints = hintOverrides ? {
|
|
842
|
+
...phpHints,
|
|
843
|
+
...hintOverrides
|
|
844
|
+
} : phpHints;
|
|
845
|
+
return {
|
|
846
|
+
language: "php",
|
|
847
|
+
hints: mergedHints,
|
|
848
|
+
async extract(sdkPath) {
|
|
849
|
+
let sourceDir;
|
|
850
|
+
const libPath = resolve(sdkPath, "lib");
|
|
851
|
+
const srcPath = resolve(sdkPath, "src");
|
|
852
|
+
try {
|
|
853
|
+
statSync(libPath);
|
|
854
|
+
sourceDir = libPath;
|
|
855
|
+
} catch {
|
|
856
|
+
try {
|
|
857
|
+
statSync(srcPath);
|
|
858
|
+
sourceDir = srcPath;
|
|
859
|
+
} catch {
|
|
860
|
+
throw new ExtractorError(`No lib/ or src/ directory found in ${sdkPath}`, `Ensure the --sdk-path argument points to a PHP project root containing a lib/ or src/ directory.`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const phpFiles = walkPhpFiles(sourceDir);
|
|
864
|
+
if (phpFiles.length === 0) throw new ExtractorError(`No .php files found in ${sourceDir}`, `Ensure the source directory contains PHP source files.`);
|
|
865
|
+
const allClasses = [];
|
|
866
|
+
for (const filePath of phpFiles) {
|
|
867
|
+
const parsed = parsePhpFile(filePath, sdkPath);
|
|
868
|
+
allClasses.push(...parsed.classes);
|
|
869
|
+
}
|
|
870
|
+
const { classes, interfaces, enums, exports } = buildSurface$6(allClasses, mergedHints);
|
|
871
|
+
return {
|
|
872
|
+
language: "php",
|
|
873
|
+
extractedFrom: sdkPath,
|
|
874
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
875
|
+
classes: sortRecord(classes),
|
|
876
|
+
interfaces: sortRecord(interfaces),
|
|
877
|
+
typeAliases: {},
|
|
878
|
+
enums: sortRecord(enums),
|
|
879
|
+
exports: sortRecord(exports)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/** Default PHP extractor with generic language hints (no SDK-specific bases). */
|
|
885
|
+
const phpExtractor = createPhpExtractor();
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/compat/extractors/python-parser.ts
|
|
888
|
+
/** Python source parser — tree-sitter-based extraction of classes, type aliases, and symbols. */
|
|
889
|
+
const pythonParser = new Parser();
|
|
890
|
+
pythonParser.setLanguage(Python);
|
|
891
|
+
function walkPythonFiles(dir) {
|
|
892
|
+
const results = [];
|
|
893
|
+
let entries;
|
|
894
|
+
try {
|
|
895
|
+
entries = readdirSync(dir);
|
|
896
|
+
} catch {
|
|
897
|
+
return results;
|
|
898
|
+
}
|
|
899
|
+
for (const entry of entries) {
|
|
900
|
+
const fullPath = join(dir, entry);
|
|
901
|
+
let stat;
|
|
902
|
+
try {
|
|
903
|
+
stat = statSync(fullPath);
|
|
904
|
+
} catch {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (stat.isDirectory()) {
|
|
908
|
+
if (entry.startsWith(".") || entry === "__pycache__" || entry === "tests" || entry === "test" || entry === "node_modules") continue;
|
|
909
|
+
results.push(...walkPythonFiles(fullPath));
|
|
910
|
+
} else if (entry.endsWith(".py")) {
|
|
911
|
+
if (entry.endsWith("_test.py") || entry.startsWith("test_")) continue;
|
|
912
|
+
if (entry.startsWith("_") && entry !== "__init__.py") continue;
|
|
913
|
+
results.push(fullPath);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return results.sort();
|
|
917
|
+
}
|
|
918
|
+
/** Find the Python source root (prefers `src/`, falls back to top-level package). */
|
|
919
|
+
function findPythonSourceRoot(sdkPath) {
|
|
920
|
+
const srcDir = join(sdkPath, "src");
|
|
921
|
+
try {
|
|
922
|
+
if (statSync(srcDir).isDirectory()) {
|
|
923
|
+
const entries = readdirSync(srcDir);
|
|
924
|
+
for (const entry of entries) {
|
|
925
|
+
const pkgDir = join(srcDir, entry);
|
|
926
|
+
try {
|
|
927
|
+
if (statSync(pkgDir).isDirectory()) {
|
|
928
|
+
const initFile = join(pkgDir, "__init__.py");
|
|
929
|
+
try {
|
|
930
|
+
statSync(initFile);
|
|
931
|
+
return srcDir;
|
|
932
|
+
} catch {}
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} catch {}
|
|
940
|
+
try {
|
|
941
|
+
const entries = readdirSync(sdkPath);
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
if (entry.startsWith(".") || entry === "tests" || entry === "test" || entry === "node_modules") continue;
|
|
944
|
+
const pkgDir = join(sdkPath, entry);
|
|
945
|
+
try {
|
|
946
|
+
if (statSync(pkgDir).isDirectory()) {
|
|
947
|
+
const initFile = join(pkgDir, "__init__.py");
|
|
948
|
+
try {
|
|
949
|
+
statSync(initFile);
|
|
950
|
+
return sdkPath;
|
|
951
|
+
} catch {}
|
|
952
|
+
}
|
|
953
|
+
} catch {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
} catch {}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
/** Get all decorator names from a decorated_definition or class_definition. */
|
|
961
|
+
function getDecorators(node) {
|
|
962
|
+
const decorators = [];
|
|
963
|
+
if (node.type === "decorated_definition") {
|
|
964
|
+
for (const child of node.namedChildren) if (child.type === "decorator") {
|
|
965
|
+
const exprNode = child.namedChildren[0];
|
|
966
|
+
if (exprNode) decorators.push(exprNode.text);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return decorators;
|
|
970
|
+
}
|
|
971
|
+
/** Extract base class names from a class definition's argument_list. */
|
|
972
|
+
function getBaseClasses(classNode) {
|
|
973
|
+
const bases = [];
|
|
974
|
+
const argList = classNode.childForFieldName("superclasses");
|
|
975
|
+
if (!argList) return bases;
|
|
976
|
+
for (const child of argList.namedChildren) if (child.type === "identifier" || child.type === "attribute") bases.push(child.text);
|
|
977
|
+
else if (child.type === "keyword_argument") {}
|
|
978
|
+
return bases;
|
|
979
|
+
}
|
|
980
|
+
/** Extract annotated fields from a class body. */
|
|
981
|
+
function extractClassFields(bodyNode) {
|
|
982
|
+
const fields = [];
|
|
983
|
+
for (const child of bodyNode.namedChildren) {
|
|
984
|
+
if (child.type !== "expression_statement") continue;
|
|
985
|
+
const stmtChild = child.namedChildren[0];
|
|
986
|
+
if (!stmtChild || stmtChild.type !== "assignment") continue;
|
|
987
|
+
const left = stmtChild.childForFieldName("left");
|
|
988
|
+
const typeNode = stmtChild.childForFieldName("type");
|
|
989
|
+
if (!left || !typeNode) continue;
|
|
990
|
+
if (left.type !== "identifier") continue;
|
|
991
|
+
const right = stmtChild.childForFieldName("right");
|
|
992
|
+
fields.push({
|
|
993
|
+
name: left.text,
|
|
994
|
+
type: typeNode.text,
|
|
995
|
+
hasDefault: right !== null
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return fields;
|
|
999
|
+
}
|
|
1000
|
+
/** Extract methods from a class body. */
|
|
1001
|
+
function extractClassMethods(bodyNode) {
|
|
1002
|
+
const methods = [];
|
|
1003
|
+
for (const child of bodyNode.namedChildren) {
|
|
1004
|
+
let funcNode = null;
|
|
1005
|
+
let decorators = [];
|
|
1006
|
+
if (child.type === "decorated_definition") {
|
|
1007
|
+
decorators = getDecorators(child);
|
|
1008
|
+
funcNode = child.namedChildren.find((c) => c.type === "function_definition") || null;
|
|
1009
|
+
} else if (child.type === "function_definition") funcNode = child;
|
|
1010
|
+
if (!funcNode) continue;
|
|
1011
|
+
const nameNode = funcNode.childForFieldName("name");
|
|
1012
|
+
if (!nameNode) continue;
|
|
1013
|
+
const methodName = nameNode.text;
|
|
1014
|
+
if (methodName.startsWith("_") && methodName !== "__init__") continue;
|
|
1015
|
+
const isAsync = funcNode.namedChildren.some((c) => c.type === "async");
|
|
1016
|
+
const isProperty = decorators.includes("property");
|
|
1017
|
+
const isClassMethod = decorators.includes("classmethod");
|
|
1018
|
+
const isStaticMethod = decorators.includes("staticmethod");
|
|
1019
|
+
const params = extractFunctionParams(funcNode.childForFieldName("parameters"), isStaticMethod || isClassMethod);
|
|
1020
|
+
const returnTypeNode = funcNode.childForFieldName("return_type");
|
|
1021
|
+
const returnType = returnTypeNode ? returnTypeNode.text : "";
|
|
1022
|
+
methods.push({
|
|
1023
|
+
name: methodName,
|
|
1024
|
+
params,
|
|
1025
|
+
returnType,
|
|
1026
|
+
isAsync,
|
|
1027
|
+
isProperty,
|
|
1028
|
+
isClassMethod,
|
|
1029
|
+
isStaticMethod
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return methods;
|
|
1033
|
+
}
|
|
1034
|
+
/** Extract parameters from a function's parameter list. */
|
|
1035
|
+
function extractFunctionParams(paramsNode, skipFirst) {
|
|
1036
|
+
if (!paramsNode) return [];
|
|
1037
|
+
const params = [];
|
|
1038
|
+
let skippedFirst = false;
|
|
1039
|
+
for (const child of paramsNode.namedChildren) {
|
|
1040
|
+
if (!skippedFirst && !skipFirst && child.type === "identifier" && (child.text === "self" || child.text === "cls")) {
|
|
1041
|
+
skippedFirst = true;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (!skippedFirst && !skipFirst && child.type === "typed_parameter" && child.namedChildren[0]?.text === "self") {
|
|
1045
|
+
skippedFirst = true;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (child.text === "*") continue;
|
|
1049
|
+
if (child.type === "list_splat_pattern") {
|
|
1050
|
+
const nameNode = child.namedChildren.find((c) => c.type === "identifier");
|
|
1051
|
+
params.push({
|
|
1052
|
+
name: nameNode ? `*${nameNode.text}` : "*args",
|
|
1053
|
+
type: "",
|
|
1054
|
+
optional: true
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (child.type === "dictionary_splat_pattern") {
|
|
1059
|
+
const nameNode = child.namedChildren.find((c) => c.type === "identifier");
|
|
1060
|
+
params.push({
|
|
1061
|
+
name: nameNode ? `**${nameNode.text}` : "**kwargs",
|
|
1062
|
+
type: "",
|
|
1063
|
+
optional: true
|
|
1064
|
+
});
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (child.type === "typed_parameter") {
|
|
1068
|
+
const nameNode = child.namedChildren.find((c) => c.type === "identifier");
|
|
1069
|
+
const typeNode = child.childForFieldName("type");
|
|
1070
|
+
if (nameNode) params.push({
|
|
1071
|
+
name: nameNode.text,
|
|
1072
|
+
type: typeNode ? typeNode.text : "",
|
|
1073
|
+
optional: false
|
|
1074
|
+
});
|
|
1075
|
+
} else if (child.type === "typed_default_parameter") {
|
|
1076
|
+
const nameNode = child.childForFieldName("name");
|
|
1077
|
+
const typeNode = child.childForFieldName("type");
|
|
1078
|
+
if (nameNode) params.push({
|
|
1079
|
+
name: nameNode.text,
|
|
1080
|
+
type: typeNode ? typeNode.text : "",
|
|
1081
|
+
optional: true
|
|
1082
|
+
});
|
|
1083
|
+
} else if (child.type === "default_parameter") {
|
|
1084
|
+
const nameNode = child.childForFieldName("name");
|
|
1085
|
+
if (nameNode) params.push({
|
|
1086
|
+
name: nameNode.text,
|
|
1087
|
+
type: "",
|
|
1088
|
+
optional: true
|
|
1089
|
+
});
|
|
1090
|
+
} else if (child.type === "identifier") params.push({
|
|
1091
|
+
name: child.text,
|
|
1092
|
+
type: "",
|
|
1093
|
+
optional: false
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
return params;
|
|
1097
|
+
}
|
|
1098
|
+
/** Extract module-level type aliases (e.g., FooType = Literal["a", "b"]). */
|
|
1099
|
+
function extractTypeAliases(tree, sourceFile) {
|
|
1100
|
+
const aliases = [];
|
|
1101
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
1102
|
+
if (node.type === "type_alias_statement") {
|
|
1103
|
+
const nameNode = node.childForFieldName("name");
|
|
1104
|
+
const valueNode = node.childForFieldName("value");
|
|
1105
|
+
if (nameNode && valueNode) aliases.push({
|
|
1106
|
+
name: nameNode.text,
|
|
1107
|
+
value: valueNode.text,
|
|
1108
|
+
sourceFile
|
|
1109
|
+
});
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (node.type === "expression_statement") {
|
|
1113
|
+
const inner = node.namedChildren[0];
|
|
1114
|
+
if (!inner || inner.type !== "assignment") continue;
|
|
1115
|
+
const left = inner.childForFieldName("left");
|
|
1116
|
+
const right = inner.childForFieldName("right");
|
|
1117
|
+
if (!left || !right) continue;
|
|
1118
|
+
if (left.type !== "identifier") continue;
|
|
1119
|
+
const name = left.text;
|
|
1120
|
+
if (name.startsWith("_")) continue;
|
|
1121
|
+
if (/^[A-Z][A-Z_0-9]*$/.test(name)) continue;
|
|
1122
|
+
const value = right.text;
|
|
1123
|
+
if (right.type === "subscript" || right.type === "string" && value.includes("[")) {
|
|
1124
|
+
let cleanValue = value;
|
|
1125
|
+
if (right.type === "string") cleanValue = value.replace(/^["']|["']$/g, "");
|
|
1126
|
+
aliases.push({
|
|
1127
|
+
name,
|
|
1128
|
+
value: cleanValue,
|
|
1129
|
+
sourceFile
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return aliases;
|
|
1135
|
+
}
|
|
1136
|
+
/** Extract __all__ exports from a module. */
|
|
1137
|
+
function extractAllExports(tree) {
|
|
1138
|
+
const exports = [];
|
|
1139
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
1140
|
+
if (node.type !== "expression_statement") continue;
|
|
1141
|
+
const inner = node.namedChildren[0];
|
|
1142
|
+
if (!inner || inner.type !== "assignment") continue;
|
|
1143
|
+
const left = inner.childForFieldName("left");
|
|
1144
|
+
const right = inner.childForFieldName("right");
|
|
1145
|
+
if (!left || !right) continue;
|
|
1146
|
+
if (left.text !== "__all__") continue;
|
|
1147
|
+
if (right.type === "list") {
|
|
1148
|
+
for (const elem of right.namedChildren) if (elem.type === "string") {
|
|
1149
|
+
const text = elem.text.replace(/^["']|["']$/g, "");
|
|
1150
|
+
exports.push(text);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return exports;
|
|
1155
|
+
}
|
|
1156
|
+
function extractClassesFromTree(tree, sourceFile) {
|
|
1157
|
+
const classes = [];
|
|
1158
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
1159
|
+
let classNode = null;
|
|
1160
|
+
let decorators = [];
|
|
1161
|
+
if (node.type === "decorated_definition") {
|
|
1162
|
+
decorators = getDecorators(node);
|
|
1163
|
+
classNode = node.namedChildren.find((c) => c.type === "class_definition") || null;
|
|
1164
|
+
} else if (node.type === "class_definition") classNode = node;
|
|
1165
|
+
if (!classNode) continue;
|
|
1166
|
+
const nameNode = classNode.childForFieldName("name");
|
|
1167
|
+
if (!nameNode) continue;
|
|
1168
|
+
const className = nameNode.text;
|
|
1169
|
+
const bases = getBaseClasses(classNode);
|
|
1170
|
+
const bodyNode = classNode.childForFieldName("body");
|
|
1171
|
+
if (!bodyNode) {
|
|
1172
|
+
classes.push({
|
|
1173
|
+
name: className,
|
|
1174
|
+
bases,
|
|
1175
|
+
fields: [],
|
|
1176
|
+
methods: [],
|
|
1177
|
+
decorators,
|
|
1178
|
+
sourceFile
|
|
1179
|
+
});
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
const fields = extractClassFields(bodyNode);
|
|
1183
|
+
const methods = extractClassMethods(bodyNode);
|
|
1184
|
+
classes.push({
|
|
1185
|
+
name: className,
|
|
1186
|
+
bases,
|
|
1187
|
+
fields,
|
|
1188
|
+
methods,
|
|
1189
|
+
decorators,
|
|
1190
|
+
sourceFile
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return classes;
|
|
1194
|
+
}
|
|
1195
|
+
/** Parse a single Python source file and return all extracted symbols. */
|
|
1196
|
+
function parsePythonFile(filePath, sdkPath) {
|
|
1197
|
+
const source = readFileSync(filePath, "utf-8");
|
|
1198
|
+
const relPath = relative(sdkPath, filePath);
|
|
1199
|
+
const tree = safeParse$1(pythonParser, source);
|
|
1200
|
+
return {
|
|
1201
|
+
classes: extractClassesFromTree(tree, relPath),
|
|
1202
|
+
typeAliases: extractTypeAliases(tree, relPath),
|
|
1203
|
+
allExports: extractAllExports(tree),
|
|
1204
|
+
sourceFile: relPath
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/compat/extractors/python-surface.ts
|
|
1209
|
+
/** Default model base classes. Overridden by hints.modelBaseClasses when provided. */
|
|
1210
|
+
const DEFAULT_MODEL_BASES = [];
|
|
1211
|
+
/** Known dict-like base classes (→ ApiInterface). */
|
|
1212
|
+
const DICT_BASES = new Set(["TypedDict"]);
|
|
1213
|
+
/** Known protocol bases (→ ApiClass as service). */
|
|
1214
|
+
const PROTOCOL_BASES = new Set(["Protocol"]);
|
|
1215
|
+
/** Default exception bases. Overridden by hints.exceptionBaseClasses when provided. */
|
|
1216
|
+
const DEFAULT_EXCEPTION_BASES = ["Exception", "BaseException"];
|
|
1217
|
+
/** Check if a class has any of the given base classes (by simple name). */
|
|
1218
|
+
function hasBase(cls, bases) {
|
|
1219
|
+
return cls.bases.some((b) => bases.has(b));
|
|
1220
|
+
}
|
|
1221
|
+
/** Check if a class is a model class (inherits from BaseModel or configured model bases,
|
|
1222
|
+
* transitively from another class that does). */
|
|
1223
|
+
function isModelClass(cls, allClasses, modelBases) {
|
|
1224
|
+
if (hasBase(cls, modelBases)) return true;
|
|
1225
|
+
for (const base of cls.bases) {
|
|
1226
|
+
const parentCls = allClasses.get(base);
|
|
1227
|
+
if (parentCls && isModelClass(parentCls, allClasses, modelBases)) return true;
|
|
1228
|
+
}
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
/** Check if a class is an exception class. */
|
|
1232
|
+
function isExceptionClass(cls, allClasses, exceptionBases) {
|
|
1233
|
+
if (hasBase(cls, exceptionBases)) return true;
|
|
1234
|
+
for (const base of cls.bases) {
|
|
1235
|
+
const parentCls = allClasses.get(base);
|
|
1236
|
+
if (parentCls && isExceptionClass(parentCls, allClasses, exceptionBases)) return true;
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
/** Parse Literal["a", "b", "c"] into member strings. */
|
|
1241
|
+
function parseLiteralMembers(value) {
|
|
1242
|
+
const match = value.match(/^Literal\[(.+)\]$/s);
|
|
1243
|
+
if (!match) return null;
|
|
1244
|
+
const inner = match[1];
|
|
1245
|
+
const members = {};
|
|
1246
|
+
const parts = [];
|
|
1247
|
+
let depth = 0;
|
|
1248
|
+
let current = "";
|
|
1249
|
+
for (const char of inner) {
|
|
1250
|
+
if (char === "[" || char === "(") depth++;
|
|
1251
|
+
else if (char === "]" || char === ")") depth--;
|
|
1252
|
+
else if (char === "," && depth === 0) {
|
|
1253
|
+
parts.push(current.trim());
|
|
1254
|
+
current = "";
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
current += char;
|
|
1258
|
+
}
|
|
1259
|
+
if (current.trim()) parts.push(current.trim());
|
|
1260
|
+
for (const part of parts) {
|
|
1261
|
+
const stripped = part.replace(/^["']|["']$/g, "");
|
|
1262
|
+
if (stripped) members[stripped] = stripped;
|
|
1263
|
+
}
|
|
1264
|
+
return Object.keys(members).length > 0 ? members : null;
|
|
1265
|
+
}
|
|
1266
|
+
/** Unwrap SyncOrAsync[T] → T. */
|
|
1267
|
+
function unwrapReturnType(returnType) {
|
|
1268
|
+
const syncOrAsyncMatch = returnType.match(/^SyncOrAsync\[(.+)\]$/);
|
|
1269
|
+
if (syncOrAsyncMatch) return syncOrAsyncMatch[1];
|
|
1270
|
+
const awaitableMatch = returnType.match(/^Awaitable\[(.+)\]$/);
|
|
1271
|
+
if (awaitableMatch) return awaitableMatch[1];
|
|
1272
|
+
return returnType;
|
|
1273
|
+
}
|
|
1274
|
+
function buildSurface$5(parsedFiles, hints) {
|
|
1275
|
+
const MODEL_BASES = new Set(hints?.modelBaseClasses ?? DEFAULT_MODEL_BASES);
|
|
1276
|
+
const EXCEPTION_BASES = new Set(hints?.exceptionBaseClasses ?? DEFAULT_EXCEPTION_BASES);
|
|
1277
|
+
const classes = {};
|
|
1278
|
+
const interfaces = {};
|
|
1279
|
+
const typeAliases = {};
|
|
1280
|
+
const enums = {};
|
|
1281
|
+
const allClassesByName = /* @__PURE__ */ new Map();
|
|
1282
|
+
for (const file of parsedFiles) for (const cls of file.classes) allClassesByName.set(cls.name, cls);
|
|
1283
|
+
const protocolNames = /* @__PURE__ */ new Set();
|
|
1284
|
+
for (const file of parsedFiles) for (const cls of file.classes) if (hasBase(cls, PROTOCOL_BASES)) protocolNames.add(cls.name);
|
|
1285
|
+
const collector = new ExportCollector();
|
|
1286
|
+
for (const file of parsedFiles) {
|
|
1287
|
+
for (const alias of file.typeAliases) {
|
|
1288
|
+
const literalMembers = parseLiteralMembers(alias.value);
|
|
1289
|
+
if (literalMembers) {
|
|
1290
|
+
enums[alias.name] = {
|
|
1291
|
+
name: alias.name,
|
|
1292
|
+
sourceFile: alias.sourceFile,
|
|
1293
|
+
members: sortRecord(literalMembers)
|
|
1294
|
+
};
|
|
1295
|
+
collector.add(alias.sourceFile, alias.name);
|
|
1296
|
+
} else {
|
|
1297
|
+
typeAliases[alias.name] = {
|
|
1298
|
+
name: alias.name,
|
|
1299
|
+
sourceFile: alias.sourceFile,
|
|
1300
|
+
value: alias.value
|
|
1301
|
+
};
|
|
1302
|
+
collector.add(alias.sourceFile, alias.name);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const cls of file.classes) {
|
|
1306
|
+
if (hasBase(cls, PROTOCOL_BASES)) {
|
|
1307
|
+
const apiMethods = {};
|
|
1308
|
+
for (const method of cls.methods) {
|
|
1309
|
+
if (method.name === "__init__") continue;
|
|
1310
|
+
const params = method.params.map((p) => ({
|
|
1311
|
+
name: p.name,
|
|
1312
|
+
type: p.type,
|
|
1313
|
+
optional: p.optional
|
|
1314
|
+
}));
|
|
1315
|
+
const returnType = unwrapReturnType(method.returnType);
|
|
1316
|
+
if (!apiMethods[method.name]) apiMethods[method.name] = [];
|
|
1317
|
+
apiMethods[method.name].push({
|
|
1318
|
+
name: method.name,
|
|
1319
|
+
params,
|
|
1320
|
+
returnType,
|
|
1321
|
+
async: method.isAsync
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
classes[cls.name] = {
|
|
1325
|
+
name: cls.name,
|
|
1326
|
+
sourceFile: cls.sourceFile,
|
|
1327
|
+
methods: sortRecord(apiMethods),
|
|
1328
|
+
properties: {},
|
|
1329
|
+
constructorParams: []
|
|
1330
|
+
};
|
|
1331
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (isModelClass(cls, allClassesByName, MODEL_BASES)) {
|
|
1335
|
+
const fields = {};
|
|
1336
|
+
for (const field of cls.fields) fields[field.name] = {
|
|
1337
|
+
name: field.name,
|
|
1338
|
+
type: field.type,
|
|
1339
|
+
optional: field.hasDefault
|
|
1340
|
+
};
|
|
1341
|
+
const extendsArr = cls.bases.filter((b) => !MODEL_BASES.has(b) && allClassesByName.has(b));
|
|
1342
|
+
interfaces[cls.name] = {
|
|
1343
|
+
name: cls.name,
|
|
1344
|
+
sourceFile: cls.sourceFile,
|
|
1345
|
+
fields: sortRecord(fields),
|
|
1346
|
+
extends: extendsArr
|
|
1347
|
+
};
|
|
1348
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (hasBase(cls, DICT_BASES)) {
|
|
1352
|
+
const fields = {};
|
|
1353
|
+
for (const field of cls.fields) fields[field.name] = {
|
|
1354
|
+
name: field.name,
|
|
1355
|
+
type: field.type,
|
|
1356
|
+
optional: field.hasDefault
|
|
1357
|
+
};
|
|
1358
|
+
interfaces[cls.name] = {
|
|
1359
|
+
name: cls.name,
|
|
1360
|
+
sourceFile: cls.sourceFile,
|
|
1361
|
+
fields: sortRecord(fields),
|
|
1362
|
+
extends: []
|
|
1363
|
+
};
|
|
1364
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (isExceptionClass(cls, allClassesByName, EXCEPTION_BASES)) {
|
|
1368
|
+
const apiMethods = {};
|
|
1369
|
+
for (const method of cls.methods) {
|
|
1370
|
+
if (method.name === "__init__") continue;
|
|
1371
|
+
const params = method.params.map((p) => ({
|
|
1372
|
+
name: p.name,
|
|
1373
|
+
type: p.type,
|
|
1374
|
+
optional: p.optional
|
|
1375
|
+
}));
|
|
1376
|
+
if (!apiMethods[method.name]) apiMethods[method.name] = [];
|
|
1377
|
+
apiMethods[method.name].push({
|
|
1378
|
+
name: method.name,
|
|
1379
|
+
params,
|
|
1380
|
+
returnType: unwrapReturnType(method.returnType),
|
|
1381
|
+
async: method.isAsync
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
const fields = {};
|
|
1385
|
+
for (const field of cls.fields) fields[field.name] = {
|
|
1386
|
+
name: field.name,
|
|
1387
|
+
type: field.type,
|
|
1388
|
+
optional: field.hasDefault
|
|
1389
|
+
};
|
|
1390
|
+
classes[cls.name] = {
|
|
1391
|
+
name: cls.name,
|
|
1392
|
+
sourceFile: cls.sourceFile,
|
|
1393
|
+
methods: sortRecord(apiMethods),
|
|
1394
|
+
properties: {},
|
|
1395
|
+
constructorParams: []
|
|
1396
|
+
};
|
|
1397
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (cls.methods.length > 0) {
|
|
1401
|
+
const apiMethods = {};
|
|
1402
|
+
const constructorParams = [];
|
|
1403
|
+
for (const method of cls.methods) {
|
|
1404
|
+
if (method.name === "__init__") {
|
|
1405
|
+
constructorParams.push(...method.params.map((p) => ({
|
|
1406
|
+
name: p.name,
|
|
1407
|
+
type: p.type,
|
|
1408
|
+
optional: p.optional
|
|
1409
|
+
})));
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const params = method.params.map((p) => ({
|
|
1413
|
+
name: p.name,
|
|
1414
|
+
type: p.type,
|
|
1415
|
+
optional: p.optional
|
|
1416
|
+
}));
|
|
1417
|
+
if (!apiMethods[method.name]) apiMethods[method.name] = [];
|
|
1418
|
+
apiMethods[method.name].push({
|
|
1419
|
+
name: method.name,
|
|
1420
|
+
params,
|
|
1421
|
+
returnType: unwrapReturnType(method.returnType),
|
|
1422
|
+
async: method.isAsync
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
classes[cls.name] = {
|
|
1426
|
+
name: cls.name,
|
|
1427
|
+
sourceFile: cls.sourceFile,
|
|
1428
|
+
methods: sortRecord(apiMethods),
|
|
1429
|
+
properties: {},
|
|
1430
|
+
constructorParams
|
|
1431
|
+
};
|
|
1432
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (cls.fields.length > 0) {
|
|
1436
|
+
const fields = {};
|
|
1437
|
+
for (const field of cls.fields) fields[field.name] = {
|
|
1438
|
+
name: field.name,
|
|
1439
|
+
type: field.type,
|
|
1440
|
+
optional: field.hasDefault
|
|
1441
|
+
};
|
|
1442
|
+
interfaces[cls.name] = {
|
|
1443
|
+
name: cls.name,
|
|
1444
|
+
sourceFile: cls.sourceFile,
|
|
1445
|
+
fields: sortRecord(fields),
|
|
1446
|
+
extends: []
|
|
1447
|
+
};
|
|
1448
|
+
collector.add(cls.sourceFile, cls.name);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (file.allExports.length > 0) for (const name of file.allExports) collector.add(file.sourceFile, name);
|
|
1452
|
+
}
|
|
1453
|
+
const exports = collector.toRecord();
|
|
1454
|
+
return {
|
|
1455
|
+
classes: sortRecord(classes),
|
|
1456
|
+
interfaces: sortRecord(interfaces),
|
|
1457
|
+
typeAliases: sortRecord(typeAliases),
|
|
1458
|
+
enums: sortRecord(enums),
|
|
1459
|
+
exports: sortRecord(exports)
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/compat/extractors/python.ts
|
|
1464
|
+
/**
|
|
1465
|
+
* Python API surface extractor.
|
|
1466
|
+
*
|
|
1467
|
+
* Walks `.py` files under `src/` (fallback to top-level package dirs),
|
|
1468
|
+
* parses classes, type aliases, and module exports, then builds an
|
|
1469
|
+
* `ApiSurface` with Protocol-based services, Pydantic model interfaces,
|
|
1470
|
+
* Literal type enums, and exception classes.
|
|
1471
|
+
*
|
|
1472
|
+
* Parser: `python-parser.ts` — tree-sitter-based Python source analysis
|
|
1473
|
+
* Surface: `python-surface.ts` — ApiSurface construction
|
|
1474
|
+
*/
|
|
1475
|
+
const PYTHON_PRIMITIVE_TYPES = new Set([
|
|
1476
|
+
"str",
|
|
1477
|
+
"int",
|
|
1478
|
+
"float",
|
|
1479
|
+
"bool",
|
|
1480
|
+
"bytes"
|
|
1481
|
+
]);
|
|
1482
|
+
const pythonHints = {
|
|
1483
|
+
stripNullable(type) {
|
|
1484
|
+
const normalized = type.replace(/\s+/g, " ").trim();
|
|
1485
|
+
const optMatch = normalized.match(/^Optional\[(.+)\]$/);
|
|
1486
|
+
if (optMatch) return optMatch[1].trim();
|
|
1487
|
+
const nrMatch = normalized.match(/^NotRequired\[(.+)\]$/);
|
|
1488
|
+
if (nrMatch) return nrMatch[1].trim();
|
|
1489
|
+
const parts = normalized.split("|").map((p) => p.trim()).filter((p) => p !== "None");
|
|
1490
|
+
if (parts.length < normalized.split("|").length) return parts.join(" | ");
|
|
1491
|
+
return null;
|
|
1492
|
+
},
|
|
1493
|
+
isNullableOnlyDifference(a, b) {
|
|
1494
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
1495
|
+
},
|
|
1496
|
+
isUnionReorder(a, b) {
|
|
1497
|
+
const parseUnion = (t) => {
|
|
1498
|
+
const unionMatch = t.match(/^Union\[(.+)\]$/);
|
|
1499
|
+
if (unionMatch) return unionMatch[1].split(",").map((s) => s.trim()).sort();
|
|
1500
|
+
return t.split("|").map((s) => s.trim()).sort();
|
|
1501
|
+
};
|
|
1502
|
+
return a !== b && parseUnion(a).join(",") === parseUnion(b).join(",");
|
|
1503
|
+
},
|
|
1504
|
+
isGenericTypeParam(type) {
|
|
1505
|
+
return /^[A-Z]$/.test(type) || /^T[A-Z][a-zA-Z]*$/.test(type);
|
|
1506
|
+
},
|
|
1507
|
+
isExtractionArtifact(type) {
|
|
1508
|
+
return type === "Any" || type === "object";
|
|
1509
|
+
},
|
|
1510
|
+
tolerateCategoryMismatch: true,
|
|
1511
|
+
extractReturnTypeName(returnType) {
|
|
1512
|
+
let inner = returnType;
|
|
1513
|
+
const syncOrAsyncMatch = inner.match(/^SyncOrAsync\[(.+)\]$/);
|
|
1514
|
+
if (syncOrAsyncMatch) inner = syncOrAsyncMatch[1];
|
|
1515
|
+
const awaitableMatch = inner.match(/^Awaitable\[(.+)\]$/);
|
|
1516
|
+
if (awaitableMatch) inner = awaitableMatch[1];
|
|
1517
|
+
const optMatch = inner.match(/^Optional\[(.+)\]$/);
|
|
1518
|
+
if (optMatch) inner = optMatch[1];
|
|
1519
|
+
for (const pattern of this.listResourcePatterns ?? []) {
|
|
1520
|
+
const prefix = pattern + "[";
|
|
1521
|
+
if (inner.startsWith(prefix)) {
|
|
1522
|
+
const rest = inner.slice(prefix.length);
|
|
1523
|
+
const end = rest.search(/[,\]]/);
|
|
1524
|
+
if (end > 0) {
|
|
1525
|
+
inner = rest.slice(0, end).trim();
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const seqMatch = inner.match(/^Sequence\[(.+)\]$/);
|
|
1531
|
+
if (seqMatch) inner = seqMatch[1];
|
|
1532
|
+
if ([
|
|
1533
|
+
"str",
|
|
1534
|
+
"int",
|
|
1535
|
+
"float",
|
|
1536
|
+
"bool",
|
|
1537
|
+
"None",
|
|
1538
|
+
"bytes",
|
|
1539
|
+
"dict",
|
|
1540
|
+
"list"
|
|
1541
|
+
].includes(inner)) return null;
|
|
1542
|
+
return inner;
|
|
1543
|
+
},
|
|
1544
|
+
extractParamTypeName(paramType) {
|
|
1545
|
+
let inner = paramType;
|
|
1546
|
+
const optMatch = inner.match(/^Optional\[(.+)\]$/);
|
|
1547
|
+
if (optMatch) inner = optMatch[1];
|
|
1548
|
+
if ([
|
|
1549
|
+
"str",
|
|
1550
|
+
"int",
|
|
1551
|
+
"float",
|
|
1552
|
+
"bool",
|
|
1553
|
+
"None",
|
|
1554
|
+
"bytes",
|
|
1555
|
+
"dict",
|
|
1556
|
+
"list"
|
|
1557
|
+
].includes(inner)) return null;
|
|
1558
|
+
return inner;
|
|
1559
|
+
},
|
|
1560
|
+
propertyMatchesClass(propertyName, className) {
|
|
1561
|
+
return propertyName.replace(/_/g, "").toLowerCase() === className.toLowerCase();
|
|
1562
|
+
},
|
|
1563
|
+
derivedModelNames(modelName) {
|
|
1564
|
+
return [`${modelName}Response`];
|
|
1565
|
+
},
|
|
1566
|
+
isTypeEquivalent(baselineType, candidateType, candidateSurface) {
|
|
1567
|
+
const stripNullable = (t) => {
|
|
1568
|
+
const normalized = t.replace(/\s+/g, " ").trim();
|
|
1569
|
+
const optMatch = normalized.match(/^Optional\[(.+)\]$/);
|
|
1570
|
+
if (optMatch) return optMatch[1].trim();
|
|
1571
|
+
const nrMatch = normalized.match(/^NotRequired\[(.+)\]$/);
|
|
1572
|
+
if (nrMatch) return nrMatch[1].trim();
|
|
1573
|
+
const parts = normalized.split("|").map((p) => p.trim()).filter((p) => p !== "None");
|
|
1574
|
+
if (parts.length < normalized.split("|").length) return parts.join(" | ");
|
|
1575
|
+
return normalized;
|
|
1576
|
+
};
|
|
1577
|
+
const baseClean = stripNullable(baselineType);
|
|
1578
|
+
const candClean = stripNullable(candidateType);
|
|
1579
|
+
const literalMatch = (t) => t.match(/^Literal\[(['"])(.+)\1\]$/);
|
|
1580
|
+
const baseLit = literalMatch(baseClean);
|
|
1581
|
+
const candLit = literalMatch(candClean);
|
|
1582
|
+
if (baseLit && candLit && baseLit[2] === candLit[2]) return true;
|
|
1583
|
+
const stripWrappers = (t) => {
|
|
1584
|
+
let inner = t;
|
|
1585
|
+
const louMatch = inner.match(/^LiteralOrUntyped\[(.+)\]$/);
|
|
1586
|
+
if (louMatch) inner = louMatch[1];
|
|
1587
|
+
return inner;
|
|
1588
|
+
};
|
|
1589
|
+
const baseUnwrapped = stripWrappers(baseClean);
|
|
1590
|
+
const candUnwrapped = stripWrappers(candClean);
|
|
1591
|
+
if (baseUnwrapped !== baseClean || candUnwrapped !== candClean) {
|
|
1592
|
+
if (baseUnwrapped === candClean || candUnwrapped === baseClean) return true;
|
|
1593
|
+
if (baseUnwrapped === candUnwrapped) return true;
|
|
1594
|
+
if (baseUnwrapped.startsWith("Literal[") && candidateSurface.enums[candClean]) return true;
|
|
1595
|
+
if (candUnwrapped.startsWith("Literal[") && candidateSurface.enums?.[baseClean]) return true;
|
|
1596
|
+
}
|
|
1597
|
+
const seqPattern = /^(?:Sequence|list|List)\[(.+)\]$/;
|
|
1598
|
+
const baseSeq = baseClean.match(seqPattern);
|
|
1599
|
+
const candSeq = candClean.match(seqPattern);
|
|
1600
|
+
if (baseSeq && candSeq) {
|
|
1601
|
+
if (baseSeq[1] === candSeq[1]) return true;
|
|
1602
|
+
const baseInner = baseSeq[1];
|
|
1603
|
+
const candInner = candSeq[1];
|
|
1604
|
+
if (NAMED_TYPE_RE.test(baseInner) && NAMED_TYPE_RE.test(candInner)) {
|
|
1605
|
+
if (typeExistsInSurface(candInner, candidateSurface)) {
|
|
1606
|
+
if (candInner.includes(baseInner) || baseInner.includes(candInner)) return true;
|
|
1607
|
+
if (namedTypeWordsOverlap(baseInner, candInner)) return true;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const primitiveTypes = PYTHON_PRIMITIVE_TYPES;
|
|
1611
|
+
if (primitiveTypes.has(baseInner) && NAMED_TYPE_RE.test(candInner) || primitiveTypes.has(candInner) && NAMED_TYPE_RE.test(baseInner)) return true;
|
|
1612
|
+
}
|
|
1613
|
+
if (baseSeq && !candSeq) {
|
|
1614
|
+
const candListMatch = candClean.match(/^(?:list|List)\[(.+)\]$/);
|
|
1615
|
+
if (candListMatch) {
|
|
1616
|
+
const baseInner = baseSeq[1];
|
|
1617
|
+
const candInner = candListMatch[1];
|
|
1618
|
+
if (baseInner === candInner) return true;
|
|
1619
|
+
const primitiveTypes = PYTHON_PRIMITIVE_TYPES;
|
|
1620
|
+
if (primitiveTypes.has(baseInner) && NAMED_TYPE_RE.test(candInner) || primitiveTypes.has(candInner) && NAMED_TYPE_RE.test(baseInner)) return true;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const mappingPattern = /^(?:Mapping|dict|Dict)\[(.+)\]$/;
|
|
1624
|
+
const baseMapping = baseClean.match(mappingPattern);
|
|
1625
|
+
const candMapping = candClean.match(mappingPattern);
|
|
1626
|
+
if (baseMapping && candMapping) return true;
|
|
1627
|
+
const dictLikePattern = /^(?:dict|Dict|Mapping)\[str,\s*.+\]$/;
|
|
1628
|
+
if (dictLikePattern.test(baseClean) && dictLikePattern.test(candClean)) return true;
|
|
1629
|
+
const isMapType = (t) => dictLikePattern.test(t) || /^(?:Metadata|AuditLog\w*Metadata)$/.test(t);
|
|
1630
|
+
if (isMapType(baseClean) && isMapType(candClean)) return true;
|
|
1631
|
+
if (NAMED_TYPE_RE.test(baseClean) && dictLikePattern.test(candClean)) {
|
|
1632
|
+
if (/(?:Metadata|Attributes)$/.test(baseClean)) return true;
|
|
1633
|
+
}
|
|
1634
|
+
if (NAMED_TYPE_RE.test(candClean) && dictLikePattern.test(baseClean)) {
|
|
1635
|
+
if (/(?:Metadata|Attributes)$/.test(candClean)) return true;
|
|
1636
|
+
}
|
|
1637
|
+
if (baseClean === "str" && candClean.startsWith("Literal[")) return true;
|
|
1638
|
+
if (candClean === "str" && baseClean.startsWith("Literal[")) return true;
|
|
1639
|
+
if (baseClean.startsWith("Literal[") && candidateSurface.enums[candClean]) return true;
|
|
1640
|
+
if (candClean.startsWith("Literal[") && candidateSurface.enums?.[baseClean]) return true;
|
|
1641
|
+
if (baseClean === "str" && candidateSurface.enums[candClean]) return true;
|
|
1642
|
+
if (candClean === "str" && candidateSurface.enums?.[baseClean]) return true;
|
|
1643
|
+
const numericTypes = new Set(["int", "float"]);
|
|
1644
|
+
if (numericTypes.has(baseClean) && numericTypes.has(candClean)) return true;
|
|
1645
|
+
if (NAMED_TYPE_RE.test(baseClean) && NAMED_TYPE_RE.test(candClean)) {
|
|
1646
|
+
if (typeExistsInSurface(candClean, candidateSurface)) {
|
|
1647
|
+
if (candClean.includes(baseClean) || baseClean.includes(candClean)) return true;
|
|
1648
|
+
const baseNoResp = baseClean.replace(/Response$/, "");
|
|
1649
|
+
const candNoResp = candClean.replace(/Response$/, "");
|
|
1650
|
+
if (candNoResp.includes(baseNoResp) || baseNoResp.includes(candNoResp)) return true;
|
|
1651
|
+
if (namedTypeWordsOverlap(baseClean, candClean)) return true;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return false;
|
|
1655
|
+
},
|
|
1656
|
+
isSignatureEquivalent(baseline, candidate, _candidateSurface) {
|
|
1657
|
+
if (baseline.returnType !== candidate.returnType) {
|
|
1658
|
+
const baseRet = baseline.returnType;
|
|
1659
|
+
const candRet = candidate.returnType;
|
|
1660
|
+
if (!(NAMED_TYPE_RE.test(baseRet) && NAMED_TYPE_RE.test(candRet))) return false;
|
|
1661
|
+
if (!candRet.includes(baseRet) && !baseRet.includes(candRet)) {
|
|
1662
|
+
if (!namedTypeWordsOverlap(baseRet, candRet)) return false;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
const bodyDictTypes = new Set([
|
|
1666
|
+
"dict[str, Any]",
|
|
1667
|
+
"Dict[str, Any]",
|
|
1668
|
+
"dict"
|
|
1669
|
+
]);
|
|
1670
|
+
const candBodyIdx = candidate.params.findIndex((p) => bodyDictTypes.has(p.type) && (p.name === "payload" || p.name === "body" || p.name === "data"));
|
|
1671
|
+
if (candBodyIdx < 0) return false;
|
|
1672
|
+
for (let i = 0; i < candBodyIdx; i++) {
|
|
1673
|
+
if (i >= baseline.params.length) return false;
|
|
1674
|
+
if (baseline.params[i].name !== candidate.params[i].name) return false;
|
|
1675
|
+
if (baseline.params[i].type !== candidate.params[i].type) return false;
|
|
1676
|
+
}
|
|
1677
|
+
return true;
|
|
1678
|
+
},
|
|
1679
|
+
modelBaseClasses: [],
|
|
1680
|
+
exceptionBaseClasses: ["Exception", "BaseException"],
|
|
1681
|
+
listResourcePatterns: []
|
|
1682
|
+
};
|
|
1683
|
+
/** Create a Python extractor with optional hint overrides. */
|
|
1684
|
+
function createPythonExtractor(hintOverrides) {
|
|
1685
|
+
const mergedHints = hintOverrides ? {
|
|
1686
|
+
...pythonHints,
|
|
1687
|
+
...hintOverrides
|
|
1688
|
+
} : pythonHints;
|
|
1689
|
+
return {
|
|
1690
|
+
language: "python",
|
|
1691
|
+
hints: mergedHints,
|
|
1692
|
+
async extract(sdkPath) {
|
|
1693
|
+
const sourceRoot = findPythonSourceRoot(sdkPath);
|
|
1694
|
+
if (!sourceRoot) throw new ExtractorError(`No Python package found in ${sdkPath}`, `Ensure the --sdk-path argument points to a Python project root containing a package directory with __init__.py.`);
|
|
1695
|
+
const pyFiles = walkPythonFiles(sourceRoot);
|
|
1696
|
+
if (pyFiles.length === 0) throw new ExtractorError(`No .py files found in ${sdkPath}`, `Ensure the project contains Python source files.`);
|
|
1697
|
+
const parsedFiles = [];
|
|
1698
|
+
for (const filePath of pyFiles) {
|
|
1699
|
+
const parsed = parsePythonFile(filePath, sdkPath);
|
|
1700
|
+
parsedFiles.push(parsed);
|
|
1701
|
+
}
|
|
1702
|
+
const { classes, interfaces, typeAliases, enums, exports } = buildSurface$5(parsedFiles, mergedHints);
|
|
1703
|
+
return {
|
|
1704
|
+
language: "python",
|
|
1705
|
+
extractedFrom: sdkPath,
|
|
1706
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1707
|
+
classes,
|
|
1708
|
+
interfaces,
|
|
1709
|
+
typeAliases,
|
|
1710
|
+
enums,
|
|
1711
|
+
exports
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
/** Default Python extractor with generic language hints (no SDK-specific bases). */
|
|
1717
|
+
const pythonExtractor = createPythonExtractor();
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region src/compat/extractors/ruby-parser.ts
|
|
1720
|
+
/**
|
|
1721
|
+
* Ruby entity extraction — tree-sitter-based parsing of classes, service modules,
|
|
1722
|
+
* and enum modules from Ruby source files.
|
|
1723
|
+
*/
|
|
1724
|
+
const rubyParser = new Parser();
|
|
1725
|
+
rubyParser.setLanguage(Ruby);
|
|
1726
|
+
function safeParse(source) {
|
|
1727
|
+
return safeParse$1(rubyParser, source);
|
|
1728
|
+
}
|
|
1729
|
+
/** Parse a Ruby source string and return all class declarations (excluding service modules). */
|
|
1730
|
+
function extractClasses(source) {
|
|
1731
|
+
const tree = safeParse(source);
|
|
1732
|
+
const classes = [];
|
|
1733
|
+
const serviceRanges = findServiceModulesFromTree(tree.rootNode).map((n) => [n.startPosition.row, n.endPosition.row]);
|
|
1734
|
+
for (const node of tree.rootNode.descendantsOfType("class")) {
|
|
1735
|
+
if (node.type !== "class") continue;
|
|
1736
|
+
if (isInsideRanges(node.startPosition.row, serviceRanges)) continue;
|
|
1737
|
+
if (node.descendantsOfType("singleton_class").length > 0) continue;
|
|
1738
|
+
const nameNode = node.childForFieldName("name");
|
|
1739
|
+
if (!nameNode) continue;
|
|
1740
|
+
const className = nameNode.text;
|
|
1741
|
+
const bodyNode = node.childForFieldName("body");
|
|
1742
|
+
const methods = {};
|
|
1743
|
+
const properties = {};
|
|
1744
|
+
const constructorParams = [];
|
|
1745
|
+
if (bodyNode) {
|
|
1746
|
+
extractAttributes(bodyNode, properties);
|
|
1747
|
+
const extractedMethods = extractMethodsFromBody(bodyNode);
|
|
1748
|
+
for (const method of extractedMethods) if (method.name === "initialize") constructorParams.push(...method.params);
|
|
1749
|
+
else {
|
|
1750
|
+
if (!methods[method.name]) methods[method.name] = [];
|
|
1751
|
+
methods[method.name].push(method);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
classes.push({
|
|
1755
|
+
name: className,
|
|
1756
|
+
methods: sortRecord(methods),
|
|
1757
|
+
properties: sortRecord(properties),
|
|
1758
|
+
constructorParams
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
return classes;
|
|
1762
|
+
}
|
|
1763
|
+
/** Extract service modules (modules with `class << self` at immediate depth). */
|
|
1764
|
+
function extractServiceModules(source) {
|
|
1765
|
+
const tree = safeParse(source);
|
|
1766
|
+
const services = [];
|
|
1767
|
+
for (const moduleNode of findServiceModulesFromTree(tree.rootNode)) {
|
|
1768
|
+
const nameNode = moduleNode.childForFieldName("name");
|
|
1769
|
+
if (!nameNode) continue;
|
|
1770
|
+
const moduleName = nameNode.text;
|
|
1771
|
+
const bodyNode = moduleNode.childForFieldName("body");
|
|
1772
|
+
if (!bodyNode) continue;
|
|
1773
|
+
const singletonNode = findImmediateSingletonClass(bodyNode);
|
|
1774
|
+
if (!singletonNode) continue;
|
|
1775
|
+
const singletonBody = singletonNode.childForFieldName("body");
|
|
1776
|
+
if (!singletonBody) continue;
|
|
1777
|
+
const methods = {};
|
|
1778
|
+
const properties = {};
|
|
1779
|
+
const extractedMethods = extractMethodsFromBody(singletonBody);
|
|
1780
|
+
for (const method of extractedMethods) {
|
|
1781
|
+
if (!methods[method.name]) methods[method.name] = [];
|
|
1782
|
+
methods[method.name].push(method);
|
|
1783
|
+
}
|
|
1784
|
+
extractConstantsAsProperties(singletonBody, properties);
|
|
1785
|
+
if (Object.keys(methods).length > 0 || Object.keys(properties).length > 0) services.push({
|
|
1786
|
+
name: moduleName,
|
|
1787
|
+
methods: sortRecord(methods),
|
|
1788
|
+
properties: sortRecord(properties),
|
|
1789
|
+
constructorParams: []
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
return services;
|
|
1793
|
+
}
|
|
1794
|
+
/** Extract enum-like modules (modules with string/number constants, no class << self). */
|
|
1795
|
+
function extractEnumModules$1(source) {
|
|
1796
|
+
const tree = safeParse(source);
|
|
1797
|
+
const enums = [];
|
|
1798
|
+
const serviceRanges = findServiceModulesFromTree(tree.rootNode).map((n) => [n.startPosition.row, n.endPosition.row]);
|
|
1799
|
+
for (const node of tree.rootNode.descendantsOfType("module")) {
|
|
1800
|
+
if (isInsideRanges(node.startPosition.row, serviceRanges)) continue;
|
|
1801
|
+
if (node.descendantsOfType("singleton_class").length > 0) continue;
|
|
1802
|
+
const nameNode = node.childForFieldName("name");
|
|
1803
|
+
if (!nameNode) continue;
|
|
1804
|
+
const bodyNode = node.childForFieldName("body");
|
|
1805
|
+
if (!bodyNode) continue;
|
|
1806
|
+
const constants = extractEnumConstants(bodyNode);
|
|
1807
|
+
if (Object.keys(constants).length < 2) continue;
|
|
1808
|
+
enums.push({
|
|
1809
|
+
name: nameNode.text,
|
|
1810
|
+
members: sortRecord(constants)
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
return enums;
|
|
1814
|
+
}
|
|
1815
|
+
/** Extract autoload declarations to build the export map. */
|
|
1816
|
+
function extractAutoloads(source) {
|
|
1817
|
+
const tree = safeParse(source);
|
|
1818
|
+
const names = [];
|
|
1819
|
+
for (const callNode of tree.rootNode.descendantsOfType("call")) {
|
|
1820
|
+
const methodNode = callNode.childForFieldName("method");
|
|
1821
|
+
if (!methodNode || methodNode.text !== "autoload") continue;
|
|
1822
|
+
const argsNode = callNode.childForFieldName("arguments");
|
|
1823
|
+
if (!argsNode) continue;
|
|
1824
|
+
const firstArg = argsNode.namedChildren[0];
|
|
1825
|
+
if (firstArg && (firstArg.type === "simple_symbol" || firstArg.type === "symbol")) names.push(firstArg.text.replace(/^:/, ""));
|
|
1826
|
+
}
|
|
1827
|
+
return names;
|
|
1828
|
+
}
|
|
1829
|
+
/** Find module nodes that have `class << self` at their immediate body level. */
|
|
1830
|
+
function findServiceModulesFromTree(rootNode) {
|
|
1831
|
+
const result = [];
|
|
1832
|
+
for (const moduleNode of rootNode.descendantsOfType("module")) {
|
|
1833
|
+
const bodyNode = moduleNode.childForFieldName("body");
|
|
1834
|
+
if (!bodyNode) continue;
|
|
1835
|
+
if (findImmediateSingletonClass(bodyNode)) result.push(moduleNode);
|
|
1836
|
+
}
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
/** Find a singleton_class (class << self) at the immediate body level (not nested). */
|
|
1840
|
+
function findImmediateSingletonClass(bodyNode) {
|
|
1841
|
+
for (const child of bodyNode.namedChildren) if (child.type === "singleton_class") return child;
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
/** Extract methods from a body_statement, respecting private/protected/public markers. */
|
|
1845
|
+
function extractMethodsFromBody(bodyNode) {
|
|
1846
|
+
const methods = [];
|
|
1847
|
+
let isPrivate = false;
|
|
1848
|
+
for (const child of bodyNode.namedChildren) {
|
|
1849
|
+
if (child.type === "identifier") {
|
|
1850
|
+
if (child.text === "private") {
|
|
1851
|
+
isPrivate = true;
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (child.text === "public") {
|
|
1855
|
+
isPrivate = false;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (child.text === "protected") {
|
|
1859
|
+
isPrivate = true;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (isPrivate) continue;
|
|
1864
|
+
if (child.type === "method") {
|
|
1865
|
+
const nameNode = child.childForFieldName("name");
|
|
1866
|
+
if (!nameNode) continue;
|
|
1867
|
+
const params = extractMethodParams(child);
|
|
1868
|
+
methods.push({
|
|
1869
|
+
name: nameNode.text,
|
|
1870
|
+
params,
|
|
1871
|
+
returnType: "Object",
|
|
1872
|
+
async: false
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return methods;
|
|
1877
|
+
}
|
|
1878
|
+
/** Extract parameters from a method node. */
|
|
1879
|
+
function extractMethodParams(methodNode) {
|
|
1880
|
+
const paramsNode = methodNode.childForFieldName("parameters");
|
|
1881
|
+
if (!paramsNode) return [];
|
|
1882
|
+
const params = [];
|
|
1883
|
+
for (const paramNode of paramsNode.namedChildren) switch (paramNode.type) {
|
|
1884
|
+
case "identifier":
|
|
1885
|
+
params.push({
|
|
1886
|
+
name: paramNode.text,
|
|
1887
|
+
type: "Object",
|
|
1888
|
+
optional: false
|
|
1889
|
+
});
|
|
1890
|
+
break;
|
|
1891
|
+
case "optional_parameter": {
|
|
1892
|
+
const nameNode = paramNode.childForFieldName("name");
|
|
1893
|
+
if (nameNode) params.push({
|
|
1894
|
+
name: nameNode.text,
|
|
1895
|
+
type: "Object",
|
|
1896
|
+
optional: true
|
|
1897
|
+
});
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
case "keyword_parameter": {
|
|
1901
|
+
const nameNode = paramNode.childForFieldName("name");
|
|
1902
|
+
if (nameNode) {
|
|
1903
|
+
const valueNode = paramNode.childForFieldName("value");
|
|
1904
|
+
params.push({
|
|
1905
|
+
name: nameNode.text,
|
|
1906
|
+
type: "Object",
|
|
1907
|
+
optional: valueNode !== null
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
break;
|
|
1911
|
+
}
|
|
1912
|
+
case "splat_parameter": {
|
|
1913
|
+
const nameNode = paramNode.childForFieldName("name");
|
|
1914
|
+
params.push({
|
|
1915
|
+
name: nameNode ? `*${nameNode.text}` : "*args",
|
|
1916
|
+
type: "Object",
|
|
1917
|
+
optional: true
|
|
1918
|
+
});
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
case "hash_splat_parameter": {
|
|
1922
|
+
const nameNode = paramNode.childForFieldName("name");
|
|
1923
|
+
params.push({
|
|
1924
|
+
name: nameNode ? `**${nameNode.text}` : "**kwargs",
|
|
1925
|
+
type: "Object",
|
|
1926
|
+
optional: true
|
|
1927
|
+
});
|
|
1928
|
+
break;
|
|
1929
|
+
}
|
|
1930
|
+
case "block_parameter": {
|
|
1931
|
+
const nameNode = paramNode.childForFieldName("name");
|
|
1932
|
+
params.push({
|
|
1933
|
+
name: nameNode ? `&${nameNode.text}` : "&block",
|
|
1934
|
+
type: "Object",
|
|
1935
|
+
optional: true
|
|
1936
|
+
});
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
default: break;
|
|
1940
|
+
}
|
|
1941
|
+
return params;
|
|
1942
|
+
}
|
|
1943
|
+
/** Extract attr_accessor/attr_reader calls into properties. */
|
|
1944
|
+
function extractAttributes(bodyNode, properties) {
|
|
1945
|
+
for (const callNode of bodyNode.descendantsOfType("call")) {
|
|
1946
|
+
const methodNode = callNode.childForFieldName("method");
|
|
1947
|
+
if (!methodNode) continue;
|
|
1948
|
+
const methodName = methodNode.text;
|
|
1949
|
+
if (methodName !== "attr_accessor" && methodName !== "attr_reader") continue;
|
|
1950
|
+
const isReadonly = methodName === "attr_reader";
|
|
1951
|
+
const argsNode = callNode.childForFieldName("arguments");
|
|
1952
|
+
if (!argsNode) continue;
|
|
1953
|
+
for (const argNode of argsNode.namedChildren) if (argNode.type === "simple_symbol" || argNode.type === "symbol") {
|
|
1954
|
+
const name = argNode.text.replace(/^:/, "");
|
|
1955
|
+
properties[name] = {
|
|
1956
|
+
name,
|
|
1957
|
+
type: "Object",
|
|
1958
|
+
readonly: isReadonly
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/** Extract UPPER_CASE constants from a body as readonly properties. */
|
|
1964
|
+
function extractConstantsAsProperties(bodyNode, properties) {
|
|
1965
|
+
for (const child of bodyNode.namedChildren) {
|
|
1966
|
+
if (child.type !== "assignment") continue;
|
|
1967
|
+
const leftNode = child.childForFieldName("left");
|
|
1968
|
+
if (!leftNode || leftNode.type !== "constant") continue;
|
|
1969
|
+
const name = leftNode.text;
|
|
1970
|
+
if (!/^[A-Z][A-Z_0-9]*$/.test(name)) continue;
|
|
1971
|
+
properties[name] = {
|
|
1972
|
+
name,
|
|
1973
|
+
type: "Object",
|
|
1974
|
+
readonly: true
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/** Extract enum-style constants (ConstantName = 'value') from a module body. */
|
|
1979
|
+
function extractEnumConstants(bodyNode) {
|
|
1980
|
+
const constants = {};
|
|
1981
|
+
for (const child of bodyNode.namedChildren) {
|
|
1982
|
+
if (child.type !== "assignment") continue;
|
|
1983
|
+
const leftNode = child.childForFieldName("left");
|
|
1984
|
+
if (!leftNode || leftNode.type !== "constant") continue;
|
|
1985
|
+
const name = leftNode.text;
|
|
1986
|
+
if (name === "ALL") continue;
|
|
1987
|
+
const rightNode = child.childForFieldName("right");
|
|
1988
|
+
if (!rightNode) continue;
|
|
1989
|
+
const value = extractLiteralValue(rightNode);
|
|
1990
|
+
if (value !== void 0) constants[name] = value;
|
|
1991
|
+
}
|
|
1992
|
+
return constants;
|
|
1993
|
+
}
|
|
1994
|
+
/** Extract a literal string or number value from a tree-sitter node. */
|
|
1995
|
+
function extractLiteralValue(node) {
|
|
1996
|
+
if (node.type === "string" || node.type === "string_content") {
|
|
1997
|
+
const text = node.text;
|
|
1998
|
+
if (text.startsWith("'") && text.endsWith("'") || text.startsWith("\"") && text.endsWith("\"")) return text.slice(1, -1);
|
|
1999
|
+
return text;
|
|
2000
|
+
}
|
|
2001
|
+
if (node.type === "integer") return Number(node.text);
|
|
2002
|
+
if (node.type === "float") return Number(node.text);
|
|
2003
|
+
if (node.type === "string") {
|
|
2004
|
+
const contentNode = node.namedChildren.find((c) => c.type === "string_content");
|
|
2005
|
+
if (contentNode) return contentNode.text;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/** Check if a line number falls inside any of the given ranges. */
|
|
2009
|
+
function isInsideRanges(line, ranges) {
|
|
2010
|
+
for (const [start, end] of ranges) if (line > start && line < end) return true;
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
//#endregion
|
|
2014
|
+
//#region src/compat/extractors/ruby.ts
|
|
2015
|
+
/**
|
|
2016
|
+
* Ruby API surface extractor.
|
|
2017
|
+
*
|
|
2018
|
+
* Walks `.rb` files under `lib/`, parses class/module declarations,
|
|
2019
|
+
* and builds an `ApiSurface` with classes, service modules, and enum modules.
|
|
2020
|
+
*
|
|
2021
|
+
* Parser: `ruby-parser.ts` — tree-sitter-based class/module/enum extraction
|
|
2022
|
+
*/
|
|
2023
|
+
const rubyHints = {
|
|
2024
|
+
stripNullable(type) {
|
|
2025
|
+
const match = type.match(/^T\.nilable\((.+)\)$/);
|
|
2026
|
+
return match ? match[1] : null;
|
|
2027
|
+
},
|
|
2028
|
+
isNullableOnlyDifference(a, b) {
|
|
2029
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
2030
|
+
},
|
|
2031
|
+
isUnionReorder(_a, _b) {
|
|
2032
|
+
return false;
|
|
2033
|
+
},
|
|
2034
|
+
isGenericTypeParam(type) {
|
|
2035
|
+
return type === "T.untyped";
|
|
2036
|
+
},
|
|
2037
|
+
isExtractionArtifact(type) {
|
|
2038
|
+
return type === "T.untyped" || type === "BasicObject";
|
|
2039
|
+
},
|
|
2040
|
+
tolerateCategoryMismatch: false,
|
|
2041
|
+
extractReturnTypeName(returnType) {
|
|
2042
|
+
return returnType || null;
|
|
2043
|
+
},
|
|
2044
|
+
extractParamTypeName(paramType) {
|
|
2045
|
+
if ([
|
|
2046
|
+
"String",
|
|
2047
|
+
"Integer",
|
|
2048
|
+
"Float",
|
|
2049
|
+
"Boolean",
|
|
2050
|
+
"NilClass"
|
|
2051
|
+
].includes(paramType)) return null;
|
|
2052
|
+
return paramType;
|
|
2053
|
+
},
|
|
2054
|
+
propertyMatchesClass(propertyName, className) {
|
|
2055
|
+
return propertyName.replace(/_/g, "").toLowerCase() === className.toLowerCase();
|
|
2056
|
+
},
|
|
2057
|
+
derivedModelNames(_modelName) {
|
|
2058
|
+
return [];
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
/** Recursively collect all .rb files under a directory. */
|
|
2062
|
+
function collectRbFiles(dir) {
|
|
2063
|
+
const result = [];
|
|
2064
|
+
const entries = readdirSync(dir);
|
|
2065
|
+
for (const entry of entries.sort()) {
|
|
2066
|
+
const full = join(dir, entry);
|
|
2067
|
+
if (statSync(full).isDirectory()) result.push(...collectRbFiles(full));
|
|
2068
|
+
else if (entry.endsWith(".rb")) result.push(full);
|
|
2069
|
+
}
|
|
2070
|
+
return result;
|
|
2071
|
+
}
|
|
2072
|
+
const rubyExtractor = {
|
|
2073
|
+
language: "ruby",
|
|
2074
|
+
hints: rubyHints,
|
|
2075
|
+
async extract(sdkPath) {
|
|
2076
|
+
const libPath = resolve(sdkPath, "lib");
|
|
2077
|
+
try {
|
|
2078
|
+
statSync(libPath);
|
|
2079
|
+
} catch {
|
|
2080
|
+
throw new ExtractorError(`No lib/ directory found in ${sdkPath}`, `Verify the --sdk-path argument points to a Ruby project root containing a lib/ directory.`);
|
|
2081
|
+
}
|
|
2082
|
+
const rbFiles = collectRbFiles(libPath);
|
|
2083
|
+
if (rbFiles.length === 0) throw new ExtractorError(`No .rb files found in ${libPath}`, `Ensure the lib/ directory contains Ruby source files.`);
|
|
2084
|
+
const classes = {};
|
|
2085
|
+
const enums = {};
|
|
2086
|
+
const exports = {};
|
|
2087
|
+
for (const absPath of rbFiles) {
|
|
2088
|
+
const relPath = relative(sdkPath, absPath);
|
|
2089
|
+
const source = readFileSync(absPath, "utf-8");
|
|
2090
|
+
const fileExports = [];
|
|
2091
|
+
const extractedClasses = extractClasses(source);
|
|
2092
|
+
for (const cls of extractedClasses) {
|
|
2093
|
+
classes[cls.name] = {
|
|
2094
|
+
...cls,
|
|
2095
|
+
sourceFile: relPath
|
|
2096
|
+
};
|
|
2097
|
+
fileExports.push(cls.name);
|
|
2098
|
+
}
|
|
2099
|
+
const extractedServices = extractServiceModules(source);
|
|
2100
|
+
for (const svc of extractedServices) {
|
|
2101
|
+
classes[svc.name] = {
|
|
2102
|
+
...svc,
|
|
2103
|
+
sourceFile: relPath
|
|
2104
|
+
};
|
|
2105
|
+
fileExports.push(svc.name);
|
|
2106
|
+
}
|
|
2107
|
+
const extractedEnums = extractEnumModules$1(source);
|
|
2108
|
+
for (const en of extractedEnums) {
|
|
2109
|
+
enums[en.name] = {
|
|
2110
|
+
...en,
|
|
2111
|
+
sourceFile: relPath
|
|
2112
|
+
};
|
|
2113
|
+
fileExports.push(en.name);
|
|
2114
|
+
}
|
|
2115
|
+
const autoloadNames = extractAutoloads(source);
|
|
2116
|
+
if (autoloadNames.length > 0) fileExports.push(...autoloadNames);
|
|
2117
|
+
if (fileExports.length > 0) exports[relPath] = [...new Set(fileExports)].sort();
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
language: "ruby",
|
|
2121
|
+
extractedFrom: sdkPath,
|
|
2122
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2123
|
+
classes: sortRecord(classes),
|
|
2124
|
+
interfaces: {},
|
|
2125
|
+
typeAliases: {},
|
|
2126
|
+
enums: sortRecord(enums),
|
|
2127
|
+
exports: sortRecord(exports)
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region src/compat/extractors/go-parser.ts
|
|
2133
|
+
/**
|
|
2134
|
+
* Go source parser — tree-sitter-based extraction of structs, functions, types,
|
|
2135
|
+
* and const blocks from Go source files.
|
|
2136
|
+
*/
|
|
2137
|
+
const goParser = new Parser();
|
|
2138
|
+
goParser.setLanguage(Go);
|
|
2139
|
+
function walkGoFiles(dir) {
|
|
2140
|
+
const results = [];
|
|
2141
|
+
let entries;
|
|
2142
|
+
try {
|
|
2143
|
+
entries = readdirSync(dir);
|
|
2144
|
+
} catch {
|
|
2145
|
+
return results;
|
|
2146
|
+
}
|
|
2147
|
+
for (const entry of entries) {
|
|
2148
|
+
const fullPath = join(dir, entry);
|
|
2149
|
+
let stat;
|
|
2150
|
+
try {
|
|
2151
|
+
stat = statSync(fullPath);
|
|
2152
|
+
} catch {
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (stat.isDirectory()) {
|
|
2156
|
+
if (entry.startsWith(".") || entry === "vendor" || entry === "testdata" || entry === "internal") continue;
|
|
2157
|
+
results.push(...walkGoFiles(fullPath));
|
|
2158
|
+
} else if (entry.endsWith(".go") && !entry.endsWith("_test.go")) results.push(fullPath);
|
|
2159
|
+
}
|
|
2160
|
+
return results.sort();
|
|
2161
|
+
}
|
|
2162
|
+
/** Get the text of a child node by field name, or null. */
|
|
2163
|
+
function fieldText(node, field) {
|
|
2164
|
+
const child = node.childForFieldName(field);
|
|
2165
|
+
return child ? child.text : null;
|
|
2166
|
+
}
|
|
2167
|
+
/** Extract the JSON field name from a struct tag. */
|
|
2168
|
+
function extractJsonTag(tagText) {
|
|
2169
|
+
const match = tagText.match(/json:"([^"]*)"/);
|
|
2170
|
+
if (!match) return null;
|
|
2171
|
+
const parts = match[1].split(",");
|
|
2172
|
+
const name = parts[0].trim();
|
|
2173
|
+
const omitempty = parts.includes("omitempty");
|
|
2174
|
+
if (name === "-") return null;
|
|
2175
|
+
return {
|
|
2176
|
+
name,
|
|
2177
|
+
omitempty
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
/** Render a type node back to its Go source representation. */
|
|
2181
|
+
function typeNodeText(node) {
|
|
2182
|
+
return node.text;
|
|
2183
|
+
}
|
|
2184
|
+
function parseStructFields$1(structNode) {
|
|
2185
|
+
const fields = [];
|
|
2186
|
+
const fieldList = structNode.namedChildren.find((c) => c.type === "field_declaration_list");
|
|
2187
|
+
if (!fieldList) return fields;
|
|
2188
|
+
for (const fieldDecl of fieldList.namedChildren) {
|
|
2189
|
+
if (fieldDecl.type !== "field_declaration") continue;
|
|
2190
|
+
const nameNodes = fieldDecl.namedChildren.filter((c) => c.type === "field_identifier");
|
|
2191
|
+
if (nameNodes.length === 0) {
|
|
2192
|
+
const typeNode = fieldDecl.namedChildren.find((c) => c.type !== "raw_string_literal" && c.type !== "interpreted_string_literal");
|
|
2193
|
+
if (typeNode) {
|
|
2194
|
+
const typeName = typeNode.text.replace(/^\*/, "");
|
|
2195
|
+
if (/^[A-Z]/.test(typeName)) fields.push({
|
|
2196
|
+
name: typeName,
|
|
2197
|
+
type: typeNode.text,
|
|
2198
|
+
optional: false
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
const typeNode = fieldDecl.namedChildren.find((c) => c.type !== "field_identifier" && c.type !== "raw_string_literal" && c.type !== "interpreted_string_literal");
|
|
2204
|
+
if (!typeNode) continue;
|
|
2205
|
+
const fieldType = typeNodeText(typeNode);
|
|
2206
|
+
const tagNode = fieldDecl.namedChildren.find((c) => c.type === "raw_string_literal" || c.type === "interpreted_string_literal");
|
|
2207
|
+
const jsonTag = tagNode ? extractJsonTag(tagNode.text) : null;
|
|
2208
|
+
for (const nameNode of nameNodes) {
|
|
2209
|
+
const name = nameNode.text;
|
|
2210
|
+
if (!/^[A-Z]/.test(name)) continue;
|
|
2211
|
+
fields.push({
|
|
2212
|
+
name,
|
|
2213
|
+
type: fieldType,
|
|
2214
|
+
jsonTag: jsonTag?.name || void 0,
|
|
2215
|
+
optional: jsonTag?.omitempty || false
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return fields;
|
|
2220
|
+
}
|
|
2221
|
+
function parseTypeDecls(tree, sourceFile, packageName) {
|
|
2222
|
+
const structs = [];
|
|
2223
|
+
const types = [];
|
|
2224
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
2225
|
+
if (node.type !== "type_declaration") continue;
|
|
2226
|
+
for (const spec of node.namedChildren) {
|
|
2227
|
+
const name = fieldText(spec, "name");
|
|
2228
|
+
if (!name || !/^[A-Z]/.test(name)) continue;
|
|
2229
|
+
if (spec.type === "type_spec") {
|
|
2230
|
+
const typeNode = spec.childForFieldName("type");
|
|
2231
|
+
if (!typeNode) continue;
|
|
2232
|
+
if (typeNode.type === "struct_type") structs.push({
|
|
2233
|
+
name,
|
|
2234
|
+
fields: parseStructFields$1(typeNode),
|
|
2235
|
+
sourceFile,
|
|
2236
|
+
packageName
|
|
2237
|
+
});
|
|
2238
|
+
else if (typeNode.type !== "interface_type") types.push({
|
|
2239
|
+
name,
|
|
2240
|
+
underlyingType: typeNodeText(typeNode),
|
|
2241
|
+
isAlias: false,
|
|
2242
|
+
sourceFile,
|
|
2243
|
+
packageName
|
|
2244
|
+
});
|
|
2245
|
+
} else if (spec.type === "type_alias") {
|
|
2246
|
+
const typeNode = spec.childForFieldName("type");
|
|
2247
|
+
if (typeNode) types.push({
|
|
2248
|
+
name,
|
|
2249
|
+
underlyingType: typeNodeText(typeNode),
|
|
2250
|
+
isAlias: true,
|
|
2251
|
+
sourceFile,
|
|
2252
|
+
packageName
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return {
|
|
2258
|
+
structs,
|
|
2259
|
+
types
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
/** Strip surrounding quotes from a string value. */
|
|
2263
|
+
function stripQuotes(value) {
|
|
2264
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
2265
|
+
const convMatch = value.match(/^\w+(?:\.\w+)?\("([^"]*)"\)$/);
|
|
2266
|
+
if (convMatch) return convMatch[1];
|
|
2267
|
+
return value;
|
|
2268
|
+
}
|
|
2269
|
+
function parseConstBlocks(tree, sourceFile, packageName) {
|
|
2270
|
+
const consts = [];
|
|
2271
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
2272
|
+
if (node.type !== "const_declaration") continue;
|
|
2273
|
+
let lastType = "";
|
|
2274
|
+
for (const spec of node.namedChildren) {
|
|
2275
|
+
if (spec.type !== "const_spec") continue;
|
|
2276
|
+
const nameNodes = spec.namedChildren.filter((c) => c.type === "identifier");
|
|
2277
|
+
if (nameNodes.length === 0) continue;
|
|
2278
|
+
const constName = nameNodes[0].text;
|
|
2279
|
+
if (!/^[A-Z]/.test(constName)) continue;
|
|
2280
|
+
const typeNode = spec.childForFieldName("type");
|
|
2281
|
+
if (typeNode) lastType = typeNode.text;
|
|
2282
|
+
const valueNode = spec.childForFieldName("value");
|
|
2283
|
+
if (valueNode) {
|
|
2284
|
+
const valueText = valueNode.text.trim();
|
|
2285
|
+
const strippedValue = stripQuotes(valueText);
|
|
2286
|
+
if (!typeNode) {
|
|
2287
|
+
const convMatch = valueText.match(/^([A-Z]\w*)\((.+)\)$/);
|
|
2288
|
+
if (convMatch) {
|
|
2289
|
+
consts.push({
|
|
2290
|
+
name: constName,
|
|
2291
|
+
typeName: convMatch[1],
|
|
2292
|
+
value: stripQuotes(convMatch[2].trim()),
|
|
2293
|
+
sourceFile,
|
|
2294
|
+
packageName
|
|
2295
|
+
});
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (lastType) consts.push({
|
|
2300
|
+
name: constName,
|
|
2301
|
+
typeName: lastType,
|
|
2302
|
+
value: strippedValue,
|
|
2303
|
+
sourceFile,
|
|
2304
|
+
packageName
|
|
2305
|
+
});
|
|
2306
|
+
} else if (lastType) consts.push({
|
|
2307
|
+
name: constName,
|
|
2308
|
+
typeName: lastType,
|
|
2309
|
+
value: constName,
|
|
2310
|
+
sourceFile,
|
|
2311
|
+
packageName
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return consts;
|
|
2316
|
+
}
|
|
2317
|
+
function parseParamList(paramListNode) {
|
|
2318
|
+
if (!paramListNode) return [];
|
|
2319
|
+
const params = [];
|
|
2320
|
+
let unnamedIndex = 0;
|
|
2321
|
+
for (const paramDecl of paramListNode.namedChildren) {
|
|
2322
|
+
if (paramDecl.type !== "parameter_declaration" && paramDecl.type !== "variadic_parameter_declaration") continue;
|
|
2323
|
+
const nameNode = paramDecl.namedChildren.find((c) => c.type === "identifier");
|
|
2324
|
+
const typeNode = paramDecl.childForFieldName("type");
|
|
2325
|
+
if (!typeNode) continue;
|
|
2326
|
+
const name = nameNode ? nameNode.text : `_arg${unnamedIndex++}`;
|
|
2327
|
+
params.push({
|
|
2328
|
+
name,
|
|
2329
|
+
type: typeNodeText(typeNode)
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
return params;
|
|
2333
|
+
}
|
|
2334
|
+
function parseReturnTypes(resultNode) {
|
|
2335
|
+
if (!resultNode) return [];
|
|
2336
|
+
if (resultNode.type === "parameter_list") return resultNode.namedChildren.filter((c) => c.type === "parameter_declaration").map((c) => {
|
|
2337
|
+
const t = c.childForFieldName("type");
|
|
2338
|
+
return t ? typeNodeText(t) : c.text;
|
|
2339
|
+
});
|
|
2340
|
+
return [typeNodeText(resultNode)];
|
|
2341
|
+
}
|
|
2342
|
+
function parseFunctions(tree, sourceFile, packageName) {
|
|
2343
|
+
const funcs = [];
|
|
2344
|
+
for (const node of tree.rootNode.namedChildren) if (node.type === "function_declaration") {
|
|
2345
|
+
const name = fieldText(node, "name");
|
|
2346
|
+
if (!name || !/^[A-Z]/.test(name)) continue;
|
|
2347
|
+
funcs.push({
|
|
2348
|
+
receiverType: null,
|
|
2349
|
+
name,
|
|
2350
|
+
params: parseParamList(node.childForFieldName("parameters")),
|
|
2351
|
+
returnTypes: parseReturnTypes(node.childForFieldName("result")),
|
|
2352
|
+
sourceFile,
|
|
2353
|
+
packageName
|
|
2354
|
+
});
|
|
2355
|
+
} else if (node.type === "method_declaration") {
|
|
2356
|
+
const name = fieldText(node, "name");
|
|
2357
|
+
if (!name || !/^[A-Z]/.test(name)) continue;
|
|
2358
|
+
const receiverNode = node.childForFieldName("receiver");
|
|
2359
|
+
let receiverType = null;
|
|
2360
|
+
if (receiverNode) {
|
|
2361
|
+
const paramDecl = receiverNode.namedChildren.find((c) => c.type === "parameter_declaration");
|
|
2362
|
+
if (paramDecl) {
|
|
2363
|
+
const typeNode = paramDecl.childForFieldName("type");
|
|
2364
|
+
if (typeNode) receiverType = typeNode.text.replace(/^\*/, "");
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
funcs.push({
|
|
2368
|
+
receiverType,
|
|
2369
|
+
name,
|
|
2370
|
+
params: parseParamList(node.childForFieldName("parameters")),
|
|
2371
|
+
returnTypes: parseReturnTypes(node.childForFieldName("result")),
|
|
2372
|
+
sourceFile,
|
|
2373
|
+
packageName
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
return funcs;
|
|
2377
|
+
}
|
|
2378
|
+
function extractPackageName$1(tree) {
|
|
2379
|
+
const pkgClause = tree.rootNode.namedChildren.find((c) => c.type === "package_clause");
|
|
2380
|
+
if (!pkgClause) return "unknown";
|
|
2381
|
+
const nameNode = pkgClause.namedChildren.find((c) => c.type === "package_identifier");
|
|
2382
|
+
return nameNode ? nameNode.text : "unknown";
|
|
2383
|
+
}
|
|
2384
|
+
/** Parse a single Go source file and return all extracted symbols. */
|
|
2385
|
+
function parseGoFile(filePath, sdkPath) {
|
|
2386
|
+
const source = readFileSync(filePath, "utf-8");
|
|
2387
|
+
const relPath = relative(sdkPath, filePath);
|
|
2388
|
+
const tree = safeParse$1(goParser, source);
|
|
2389
|
+
const packageName = extractPackageName$1(tree);
|
|
2390
|
+
const { structs, types } = parseTypeDecls(tree, relPath, packageName);
|
|
2391
|
+
return {
|
|
2392
|
+
structs,
|
|
2393
|
+
types,
|
|
2394
|
+
funcs: parseFunctions(tree, relPath, packageName),
|
|
2395
|
+
consts: parseConstBlocks(tree, relPath, packageName)
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
//#endregion
|
|
2399
|
+
//#region src/compat/extractors/go-surface.ts
|
|
2400
|
+
/**
|
|
2401
|
+
* Go surface builder — transforms parsed Go symbols into an ApiSurface
|
|
2402
|
+
* with proper package-qualified names for collision avoidance.
|
|
2403
|
+
*/
|
|
2404
|
+
function goTypeToString(t) {
|
|
2405
|
+
return t.replace(/json\.RawMessage/g, "[]byte").replace(/\bcontext\.Context\b/g, "context.Context");
|
|
2406
|
+
}
|
|
2407
|
+
function buildReturnType(returnTypes) {
|
|
2408
|
+
const nonError = returnTypes.filter((r) => r !== "error");
|
|
2409
|
+
if (nonError.length === 0) return "error";
|
|
2410
|
+
if (nonError.length === 1) return goTypeToString(nonError[0]);
|
|
2411
|
+
return `(${nonError.map(goTypeToString).join(", ")})`;
|
|
2412
|
+
}
|
|
2413
|
+
/** Get the package directory name from a source file relative path. */
|
|
2414
|
+
function getPackageDirName(relPath) {
|
|
2415
|
+
return basename(dirname(relPath)) || "root";
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* When multiple packages define the same exported symbol, qualify with the
|
|
2419
|
+
* package directory name (e.g., "organizations.Client", "sso.Client").
|
|
2420
|
+
* Unique symbols keep their bare name.
|
|
2421
|
+
*/
|
|
2422
|
+
function buildQualifiedNames(items) {
|
|
2423
|
+
const nameToPackages = /* @__PURE__ */ new Map();
|
|
2424
|
+
for (const item of items) {
|
|
2425
|
+
if (!nameToPackages.has(item.name)) nameToPackages.set(item.name, /* @__PURE__ */ new Set());
|
|
2426
|
+
nameToPackages.get(item.name).add(item.packageName);
|
|
2427
|
+
}
|
|
2428
|
+
const result = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (const item of items) {
|
|
2430
|
+
const key = `${item.sourceFile}:${item.name}`;
|
|
2431
|
+
const packages = nameToPackages.get(item.name);
|
|
2432
|
+
result.set(key, packages.size > 1 ? `${getPackageDirName(item.sourceFile)}.${item.name}` : item.name);
|
|
2433
|
+
}
|
|
2434
|
+
return result;
|
|
2435
|
+
}
|
|
2436
|
+
function buildSurface$4(allStructs, allTypes, allFuncs, allConsts) {
|
|
2437
|
+
const classes = {};
|
|
2438
|
+
const interfaces = {};
|
|
2439
|
+
const typeAliases = {};
|
|
2440
|
+
const enums = {};
|
|
2441
|
+
const constsByPkgAndType = /* @__PURE__ */ new Map();
|
|
2442
|
+
for (const c of allConsts) {
|
|
2443
|
+
const key = `${c.packageName}:${c.typeName}`;
|
|
2444
|
+
if (!constsByPkgAndType.has(key)) constsByPkgAndType.set(key, []);
|
|
2445
|
+
constsByPkgAndType.get(key).push(c);
|
|
2446
|
+
}
|
|
2447
|
+
const methodsByPkgAndReceiver = /* @__PURE__ */ new Map();
|
|
2448
|
+
const packageFuncs = [];
|
|
2449
|
+
for (const fn of allFuncs) if (fn.receiverType) {
|
|
2450
|
+
const key = `${fn.packageName}:${fn.receiverType}`;
|
|
2451
|
+
if (!methodsByPkgAndReceiver.has(key)) methodsByPkgAndReceiver.set(key, []);
|
|
2452
|
+
methodsByPkgAndReceiver.get(key).push(fn);
|
|
2453
|
+
} else packageFuncs.push(fn);
|
|
2454
|
+
const allSymbols = [];
|
|
2455
|
+
for (const s of allStructs) allSymbols.push({
|
|
2456
|
+
name: s.name,
|
|
2457
|
+
packageName: s.packageName,
|
|
2458
|
+
sourceFile: s.sourceFile
|
|
2459
|
+
});
|
|
2460
|
+
for (const t of allTypes) allSymbols.push({
|
|
2461
|
+
name: t.name,
|
|
2462
|
+
packageName: t.packageName,
|
|
2463
|
+
sourceFile: t.sourceFile
|
|
2464
|
+
});
|
|
2465
|
+
const qualifiedNames = buildQualifiedNames(allSymbols);
|
|
2466
|
+
for (const typeDecl of allTypes) {
|
|
2467
|
+
const qName = qualifiedNames.get(`${typeDecl.sourceFile}:${typeDecl.name}`) || typeDecl.name;
|
|
2468
|
+
const typeConsts = constsByPkgAndType.get(`${typeDecl.packageName}:${typeDecl.name}`);
|
|
2469
|
+
if (typeDecl.isAlias) typeAliases[qName] = {
|
|
2470
|
+
name: qName,
|
|
2471
|
+
sourceFile: typeDecl.sourceFile,
|
|
2472
|
+
value: goTypeToString(typeDecl.underlyingType)
|
|
2473
|
+
};
|
|
2474
|
+
else if (typeConsts && typeConsts.length > 0) {
|
|
2475
|
+
const members = {};
|
|
2476
|
+
for (const c of typeConsts) {
|
|
2477
|
+
const memberKey = c.name.startsWith(typeDecl.name) ? c.name.slice(typeDecl.name.length) || c.name : c.name;
|
|
2478
|
+
members[memberKey] = c.value;
|
|
2479
|
+
}
|
|
2480
|
+
enums[qName] = {
|
|
2481
|
+
name: qName,
|
|
2482
|
+
sourceFile: typeDecl.sourceFile,
|
|
2483
|
+
members: sortRecord(members)
|
|
2484
|
+
};
|
|
2485
|
+
} else typeAliases[qName] = {
|
|
2486
|
+
name: qName,
|
|
2487
|
+
sourceFile: typeDecl.sourceFile,
|
|
2488
|
+
value: goTypeToString(typeDecl.underlyingType)
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
for (const s of allStructs) {
|
|
2492
|
+
const qName = qualifiedNames.get(`${s.sourceFile}:${s.name}`) || s.name;
|
|
2493
|
+
const methods = methodsByPkgAndReceiver.get(`${s.packageName}:${s.name}`);
|
|
2494
|
+
if (methods && methods.length > 0) {
|
|
2495
|
+
const apiMethods = {};
|
|
2496
|
+
const properties = {};
|
|
2497
|
+
for (const field of s.fields) properties[field.name] = {
|
|
2498
|
+
name: field.name,
|
|
2499
|
+
type: goTypeToString(field.type),
|
|
2500
|
+
readonly: false
|
|
2501
|
+
};
|
|
2502
|
+
for (const fn of methods) {
|
|
2503
|
+
const params = fn.params.filter((p) => p.type !== "context.Context").map((p) => ({
|
|
2504
|
+
name: p.name,
|
|
2505
|
+
type: goTypeToString(p.type),
|
|
2506
|
+
optional: false
|
|
2507
|
+
}));
|
|
2508
|
+
if (!apiMethods[fn.name]) apiMethods[fn.name] = [];
|
|
2509
|
+
apiMethods[fn.name].push({
|
|
2510
|
+
name: fn.name,
|
|
2511
|
+
params,
|
|
2512
|
+
returnType: buildReturnType(fn.returnTypes),
|
|
2513
|
+
async: false
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
classes[qName] = {
|
|
2517
|
+
name: qName,
|
|
2518
|
+
sourceFile: s.sourceFile,
|
|
2519
|
+
methods: sortRecord(apiMethods),
|
|
2520
|
+
properties: sortRecord(properties),
|
|
2521
|
+
constructorParams: []
|
|
2522
|
+
};
|
|
2523
|
+
} else {
|
|
2524
|
+
const fields = {};
|
|
2525
|
+
for (const field of s.fields) {
|
|
2526
|
+
const fieldName = field.jsonTag || field.name;
|
|
2527
|
+
fields[fieldName] = {
|
|
2528
|
+
name: fieldName,
|
|
2529
|
+
type: goTypeToString(field.type),
|
|
2530
|
+
optional: field.optional
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
interfaces[qName] = {
|
|
2534
|
+
name: qName,
|
|
2535
|
+
sourceFile: s.sourceFile,
|
|
2536
|
+
fields: sortRecord(fields),
|
|
2537
|
+
extends: []
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
const collector = new ExportCollector();
|
|
2542
|
+
for (const [name, cls] of Object.entries(classes)) if (cls.sourceFile) collector.add(cls.sourceFile, name);
|
|
2543
|
+
for (const [name, iface] of Object.entries(interfaces)) if (iface.sourceFile) collector.add(iface.sourceFile, name);
|
|
2544
|
+
for (const [name, ta] of Object.entries(typeAliases)) if (ta.sourceFile) collector.add(ta.sourceFile, name);
|
|
2545
|
+
for (const [name, en] of Object.entries(enums)) if (en.sourceFile) collector.add(en.sourceFile, name);
|
|
2546
|
+
for (const c of allConsts) {
|
|
2547
|
+
const typeDecl = allTypes.find((t) => t.name === c.typeName && t.packageName === c.packageName);
|
|
2548
|
+
if (typeDecl) collector.add(typeDecl.sourceFile, c.name);
|
|
2549
|
+
}
|
|
2550
|
+
for (const fn of packageFuncs) collector.add(fn.sourceFile, fn.name);
|
|
2551
|
+
const exports = collector.toRecord();
|
|
2552
|
+
return {
|
|
2553
|
+
classes: sortRecord(classes),
|
|
2554
|
+
interfaces: sortRecord(interfaces),
|
|
2555
|
+
typeAliases: sortRecord(typeAliases),
|
|
2556
|
+
enums: sortRecord(enums),
|
|
2557
|
+
exports: sortRecord(exports)
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
//#endregion
|
|
2561
|
+
//#region src/compat/extractors/go.ts
|
|
2562
|
+
/**
|
|
2563
|
+
* Go API surface extractor.
|
|
2564
|
+
*
|
|
2565
|
+
* Walks `.go` files (excluding `_test.go` and `internal/`), parses exported
|
|
2566
|
+
* structs, functions, type declarations, and const blocks, then builds an
|
|
2567
|
+
* `ApiSurface` with package-qualified names to avoid collisions when
|
|
2568
|
+
* multiple packages define the same symbol (e.g., `Client`).
|
|
2569
|
+
*
|
|
2570
|
+
* Parser: `go-parser.ts` — regex-based Go source analysis
|
|
2571
|
+
* Surface: `go-surface.ts` — ApiSurface construction & name qualification
|
|
2572
|
+
*/
|
|
2573
|
+
const GO_ACRONYM_RE = new RegExp([
|
|
2574
|
+
"ID",
|
|
2575
|
+
"URL",
|
|
2576
|
+
"API",
|
|
2577
|
+
"HTTP",
|
|
2578
|
+
"HTTPS",
|
|
2579
|
+
"JSON",
|
|
2580
|
+
"XML",
|
|
2581
|
+
"SQL",
|
|
2582
|
+
"HTML",
|
|
2583
|
+
"CSS",
|
|
2584
|
+
"URI",
|
|
2585
|
+
"SSO",
|
|
2586
|
+
"IP",
|
|
2587
|
+
"TLS",
|
|
2588
|
+
"SSL",
|
|
2589
|
+
"DNS",
|
|
2590
|
+
"TCP",
|
|
2591
|
+
"UDP",
|
|
2592
|
+
"SSH",
|
|
2593
|
+
"JWT",
|
|
2594
|
+
"MFA",
|
|
2595
|
+
"SAML",
|
|
2596
|
+
"SCIM"
|
|
2597
|
+
].flatMap((a) => [a, a.charAt(0) + a.slice(1).toLowerCase()]).join("|"), "g");
|
|
2598
|
+
/**
|
|
2599
|
+
* Normalize a Go type string so that acronym casing differences don't cause
|
|
2600
|
+
* false mismatches. Lowercases all known Go acronyms to a canonical form.
|
|
2601
|
+
*/
|
|
2602
|
+
function normalizeGoAcronyms(type) {
|
|
2603
|
+
return type.replace(GO_ACRONYM_RE, (match) => match.toLowerCase());
|
|
2604
|
+
}
|
|
2605
|
+
const goExtractor = {
|
|
2606
|
+
language: "go",
|
|
2607
|
+
hints: {
|
|
2608
|
+
stripNullable(type) {
|
|
2609
|
+
if (type.startsWith("*")) return type.slice(1);
|
|
2610
|
+
return null;
|
|
2611
|
+
},
|
|
2612
|
+
isNullableOnlyDifference(a, b) {
|
|
2613
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
2614
|
+
},
|
|
2615
|
+
isUnionReorder(_a, _b) {
|
|
2616
|
+
return false;
|
|
2617
|
+
},
|
|
2618
|
+
isGenericTypeParam(type) {
|
|
2619
|
+
return type === "interface{}" || type === "any";
|
|
2620
|
+
},
|
|
2621
|
+
isExtractionArtifact(type) {
|
|
2622
|
+
return type === "interface{}" || type === "any";
|
|
2623
|
+
},
|
|
2624
|
+
tolerateCategoryMismatch: false,
|
|
2625
|
+
extractReturnTypeName(returnType) {
|
|
2626
|
+
return returnType.replace(/^\*/, "").replace(/\[\]/, "") || null;
|
|
2627
|
+
},
|
|
2628
|
+
extractParamTypeName(paramType) {
|
|
2629
|
+
const stripped = paramType.replace(/^\*/, "");
|
|
2630
|
+
if ([
|
|
2631
|
+
"string",
|
|
2632
|
+
"int",
|
|
2633
|
+
"int64",
|
|
2634
|
+
"float64",
|
|
2635
|
+
"bool"
|
|
2636
|
+
].includes(stripped)) return null;
|
|
2637
|
+
return stripped;
|
|
2638
|
+
},
|
|
2639
|
+
propertyMatchesClass(propertyName, className) {
|
|
2640
|
+
return propertyName.toLowerCase() === className.toLowerCase();
|
|
2641
|
+
},
|
|
2642
|
+
derivedModelNames(_modelName) {
|
|
2643
|
+
return [];
|
|
2644
|
+
},
|
|
2645
|
+
isTypeEquivalent(baselineType, candidateType, candidateSurface) {
|
|
2646
|
+
const normBase = normalizeGoAcronyms(baselineType);
|
|
2647
|
+
const normCand = normalizeGoAcronyms(candidateType);
|
|
2648
|
+
if (normBase === normCand) return true;
|
|
2649
|
+
const stripType = (t) => t.replace(/^\*/, "").replace(/^\[\]/, "");
|
|
2650
|
+
const strippedBase = stripType(normBase);
|
|
2651
|
+
const strippedCand = stripType(normCand);
|
|
2652
|
+
if (strippedBase === strippedCand) return true;
|
|
2653
|
+
const unqualBase = strippedBase.includes(".") ? strippedBase.split(".").pop() : strippedBase;
|
|
2654
|
+
const unqualCand = strippedCand.includes(".") ? strippedCand.split(".").pop() : strippedCand;
|
|
2655
|
+
if (unqualBase === unqualCand) return true;
|
|
2656
|
+
const candRaw = stripType(candidateType);
|
|
2657
|
+
if (unqualCand.endsWith(unqualBase) && unqualCand.length > unqualBase.length) {
|
|
2658
|
+
if (candidateSurface.enums[candRaw] != null || candidateSurface.interfaces[candRaw] != null || candidateSurface.classes[candRaw] != null || candidateSurface.typeAliases[candRaw] != null) return true;
|
|
2659
|
+
}
|
|
2660
|
+
return false;
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
async extract(sdkPath) {
|
|
2664
|
+
const goFiles = walkGoFiles(sdkPath);
|
|
2665
|
+
if (goFiles.length === 0) throw new ExtractorError(`No .go files found in ${sdkPath}`, `Ensure the --sdk-path argument points to a Go project root containing .go source files.`);
|
|
2666
|
+
const allStructs = [];
|
|
2667
|
+
const allTypes = [];
|
|
2668
|
+
const allFuncs = [];
|
|
2669
|
+
const allConsts = [];
|
|
2670
|
+
for (const filePath of goFiles) {
|
|
2671
|
+
const parsed = parseGoFile(filePath, sdkPath);
|
|
2672
|
+
allStructs.push(...parsed.structs);
|
|
2673
|
+
allTypes.push(...parsed.types);
|
|
2674
|
+
allFuncs.push(...parsed.funcs);
|
|
2675
|
+
allConsts.push(...parsed.consts);
|
|
2676
|
+
}
|
|
2677
|
+
const { classes, interfaces, typeAliases, enums, exports } = buildSurface$4(allStructs, allTypes, allFuncs, allConsts);
|
|
2678
|
+
return {
|
|
2679
|
+
language: "go",
|
|
2680
|
+
extractedFrom: sdkPath,
|
|
2681
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2682
|
+
classes,
|
|
2683
|
+
interfaces,
|
|
2684
|
+
typeAliases,
|
|
2685
|
+
enums,
|
|
2686
|
+
exports
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
//#endregion
|
|
2691
|
+
//#region src/compat/extractors/rust-parser.ts
|
|
2692
|
+
/**
|
|
2693
|
+
* Rust source parser — tree-sitter-based extraction of structs, enums,
|
|
2694
|
+
* impl blocks, traits, type aliases, and functions from Rust source files.
|
|
2695
|
+
*/
|
|
2696
|
+
const rustParser = new Parser();
|
|
2697
|
+
rustParser.setLanguage(RustLang);
|
|
2698
|
+
function walkRustFiles(dir) {
|
|
2699
|
+
const results = [];
|
|
2700
|
+
let entries;
|
|
2701
|
+
try {
|
|
2702
|
+
entries = readdirSync(dir);
|
|
2703
|
+
} catch {
|
|
2704
|
+
return results;
|
|
2705
|
+
}
|
|
2706
|
+
for (const entry of entries) {
|
|
2707
|
+
const fullPath = join(dir, entry);
|
|
2708
|
+
let stat;
|
|
2709
|
+
try {
|
|
2710
|
+
stat = statSync(fullPath);
|
|
2711
|
+
} catch {
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
if (stat.isDirectory()) {
|
|
2715
|
+
if (entry.startsWith(".") || entry === "target" || entry === "benches" || entry === "examples") continue;
|
|
2716
|
+
results.push(...walkRustFiles(fullPath));
|
|
2717
|
+
} else if (entry.endsWith(".rs") && !entry.endsWith("_test.rs")) results.push(fullPath);
|
|
2718
|
+
}
|
|
2719
|
+
return results.sort();
|
|
2720
|
+
}
|
|
2721
|
+
/** Extract the serde rename value from an attribute_item preceding a node. */
|
|
2722
|
+
function extractSerdeRename(attributeNode) {
|
|
2723
|
+
const attr = attributeNode.namedChildren.find((c) => c.type === "attribute");
|
|
2724
|
+
if (!attr) return null;
|
|
2725
|
+
const ident = attr.namedChildren.find((c) => c.type === "identifier");
|
|
2726
|
+
if (!ident || ident.text !== "serde") return null;
|
|
2727
|
+
const tokenTree = attr.namedChildren.find((c) => c.type === "token_tree");
|
|
2728
|
+
if (!tokenTree) return null;
|
|
2729
|
+
if (!tokenTree.namedChildren.find((c) => c.type === "identifier" && c.text === "rename")) return null;
|
|
2730
|
+
const strLit = tokenTree.namedChildren.find((c) => c.type === "string_literal");
|
|
2731
|
+
if (!strLit) return null;
|
|
2732
|
+
const content = strLit.namedChildren.find((c) => c.type === "string_content");
|
|
2733
|
+
return content ? content.text : null;
|
|
2734
|
+
}
|
|
2735
|
+
/** Check if a node is preceded by a serde(skip) or serde(skip_serializing) attribute. */
|
|
2736
|
+
function hasSerdeSkip(attributeNode) {
|
|
2737
|
+
const attr = attributeNode.namedChildren.find((c) => c.type === "attribute");
|
|
2738
|
+
if (!attr) return false;
|
|
2739
|
+
const ident = attr.namedChildren.find((c) => c.type === "identifier");
|
|
2740
|
+
if (!ident || ident.text !== "serde") return false;
|
|
2741
|
+
const tokenTree = attr.namedChildren.find((c) => c.type === "token_tree");
|
|
2742
|
+
if (!tokenTree) return false;
|
|
2743
|
+
return tokenTree.namedChildren.some((c) => c.type === "identifier" && (c.text === "skip" || c.text === "skip_serializing"));
|
|
2744
|
+
}
|
|
2745
|
+
/** Check if a type is Option<T> and return the inner type. */
|
|
2746
|
+
function isOptionType(typeText) {
|
|
2747
|
+
return typeText.startsWith("Option<") && typeText.endsWith(">");
|
|
2748
|
+
}
|
|
2749
|
+
/** Check if a node has `pub` visibility. */
|
|
2750
|
+
function isPublic(node) {
|
|
2751
|
+
return node.namedChildren.some((c) => c.type === "visibility_modifier");
|
|
2752
|
+
}
|
|
2753
|
+
function parseStructFields(bodyNode) {
|
|
2754
|
+
const fields = [];
|
|
2755
|
+
let pendingAttr = null;
|
|
2756
|
+
for (const child of bodyNode.namedChildren) {
|
|
2757
|
+
if (child.type === "attribute_item") {
|
|
2758
|
+
pendingAttr = child;
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
if (child.type === "field_declaration") {
|
|
2762
|
+
if (!isPublic(child)) {
|
|
2763
|
+
pendingAttr = null;
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
if (pendingAttr && hasSerdeSkip(pendingAttr)) {
|
|
2767
|
+
pendingAttr = null;
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const nameNode = child.namedChildren.find((c) => c.type === "field_identifier");
|
|
2771
|
+
const typeNode = child.childForFieldName("type");
|
|
2772
|
+
if (!nameNode || !typeNode) {
|
|
2773
|
+
pendingAttr = null;
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
const fieldName = nameNode.text;
|
|
2777
|
+
const fieldType = typeNode.text;
|
|
2778
|
+
const serdeRename = pendingAttr ? extractSerdeRename(pendingAttr) : null;
|
|
2779
|
+
fields.push({
|
|
2780
|
+
name: fieldName,
|
|
2781
|
+
type: fieldType,
|
|
2782
|
+
serdeRename: serdeRename || void 0,
|
|
2783
|
+
optional: isOptionType(fieldType)
|
|
2784
|
+
});
|
|
2785
|
+
pendingAttr = null;
|
|
2786
|
+
} else pendingAttr = null;
|
|
2787
|
+
}
|
|
2788
|
+
return fields;
|
|
2789
|
+
}
|
|
2790
|
+
function parseStructs(tree, sourceFile) {
|
|
2791
|
+
const structs = [];
|
|
2792
|
+
for (const node of tree.rootNode.descendantsOfType("struct_item")) {
|
|
2793
|
+
if (!isPublic(node)) continue;
|
|
2794
|
+
const nameNode = node.childForFieldName("name");
|
|
2795
|
+
if (!nameNode) continue;
|
|
2796
|
+
const bodyNode = node.childForFieldName("body");
|
|
2797
|
+
if (!bodyNode || bodyNode.type !== "field_declaration_list") {
|
|
2798
|
+
structs.push({
|
|
2799
|
+
name: nameNode.text,
|
|
2800
|
+
fields: [],
|
|
2801
|
+
sourceFile
|
|
2802
|
+
});
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
structs.push({
|
|
2806
|
+
name: nameNode.text,
|
|
2807
|
+
fields: parseStructFields(bodyNode),
|
|
2808
|
+
sourceFile
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
return structs;
|
|
2812
|
+
}
|
|
2813
|
+
function parseEnums$1(tree, sourceFile) {
|
|
2814
|
+
const enums = [];
|
|
2815
|
+
for (const node of tree.rootNode.descendantsOfType("enum_item")) {
|
|
2816
|
+
if (!isPublic(node)) continue;
|
|
2817
|
+
const nameNode = node.childForFieldName("name");
|
|
2818
|
+
if (!nameNode) continue;
|
|
2819
|
+
const bodyNode = node.childForFieldName("body");
|
|
2820
|
+
if (!bodyNode) continue;
|
|
2821
|
+
const variants = [];
|
|
2822
|
+
let pendingAttr = null;
|
|
2823
|
+
for (const child of bodyNode.namedChildren) {
|
|
2824
|
+
if (child.type === "attribute_item") {
|
|
2825
|
+
pendingAttr = child;
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
if (child.type === "enum_variant") {
|
|
2829
|
+
const variantName = child.childForFieldName("name");
|
|
2830
|
+
if (variantName) {
|
|
2831
|
+
const serdeRename = pendingAttr ? extractSerdeRename(pendingAttr) : null;
|
|
2832
|
+
variants.push({
|
|
2833
|
+
name: variantName.text,
|
|
2834
|
+
serdeRename: serdeRename || void 0
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
pendingAttr = null;
|
|
2838
|
+
} else pendingAttr = null;
|
|
2839
|
+
}
|
|
2840
|
+
if (variants.length > 0) enums.push({
|
|
2841
|
+
name: nameNode.text,
|
|
2842
|
+
variants,
|
|
2843
|
+
sourceFile
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
return enums;
|
|
2847
|
+
}
|
|
2848
|
+
function parseParams(paramsNode) {
|
|
2849
|
+
const params = [];
|
|
2850
|
+
for (const child of paramsNode.namedChildren) {
|
|
2851
|
+
if (child.type === "self_parameter") continue;
|
|
2852
|
+
if (child.type === "parameter") {
|
|
2853
|
+
const patternNode = child.childForFieldName("pattern");
|
|
2854
|
+
const typeNode = child.childForFieldName("type");
|
|
2855
|
+
if (patternNode && typeNode) params.push({
|
|
2856
|
+
name: patternNode.text,
|
|
2857
|
+
type: typeNode.text
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return params;
|
|
2862
|
+
}
|
|
2863
|
+
function extractReturnType(funcNode) {
|
|
2864
|
+
const retNode = funcNode.childForFieldName("return_type");
|
|
2865
|
+
if (!retNode) return "()";
|
|
2866
|
+
return retNode.text;
|
|
2867
|
+
}
|
|
2868
|
+
function isAsyncFunc(funcNode) {
|
|
2869
|
+
return funcNode.namedChildren.some((c) => c.type === "function_modifiers" && c.text.includes("async"));
|
|
2870
|
+
}
|
|
2871
|
+
function parseImplBlocks(tree, sourceFile) {
|
|
2872
|
+
const funcs = [];
|
|
2873
|
+
for (const implNode of tree.rootNode.descendantsOfType("impl_item")) {
|
|
2874
|
+
const typeNode = implNode.childForFieldName("type");
|
|
2875
|
+
if (!typeNode) continue;
|
|
2876
|
+
const receiverType = typeNode.text;
|
|
2877
|
+
const bodyNode = implNode.childForFieldName("body");
|
|
2878
|
+
if (!bodyNode) continue;
|
|
2879
|
+
for (const child of bodyNode.namedChildren) {
|
|
2880
|
+
if (child.type !== "function_item") continue;
|
|
2881
|
+
if (!isPublic(child)) continue;
|
|
2882
|
+
const nameNode = child.childForFieldName("name");
|
|
2883
|
+
if (!nameNode) continue;
|
|
2884
|
+
const paramsNode = child.childForFieldName("parameters");
|
|
2885
|
+
funcs.push({
|
|
2886
|
+
receiverType,
|
|
2887
|
+
name: nameNode.text,
|
|
2888
|
+
params: paramsNode ? parseParams(paramsNode) : [],
|
|
2889
|
+
returnType: extractReturnType(child),
|
|
2890
|
+
isAsync: isAsyncFunc(child),
|
|
2891
|
+
sourceFile
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
return funcs;
|
|
2896
|
+
}
|
|
2897
|
+
function parseTraits(tree, sourceFile) {
|
|
2898
|
+
const traits = [];
|
|
2899
|
+
for (const traitNode of tree.rootNode.descendantsOfType("trait_item")) {
|
|
2900
|
+
if (!isPublic(traitNode)) continue;
|
|
2901
|
+
const nameNode = traitNode.childForFieldName("name");
|
|
2902
|
+
if (!nameNode) continue;
|
|
2903
|
+
const bodyNode = traitNode.childForFieldName("body");
|
|
2904
|
+
if (!bodyNode) continue;
|
|
2905
|
+
const methods = [];
|
|
2906
|
+
for (const child of bodyNode.namedChildren) {
|
|
2907
|
+
if (child.type !== "function_signature_item" && child.type !== "function_item") continue;
|
|
2908
|
+
const methodName = child.childForFieldName("name");
|
|
2909
|
+
if (!methodName) continue;
|
|
2910
|
+
const paramsNode = child.childForFieldName("parameters");
|
|
2911
|
+
methods.push({
|
|
2912
|
+
receiverType: null,
|
|
2913
|
+
name: methodName.text,
|
|
2914
|
+
params: paramsNode ? parseParams(paramsNode) : [],
|
|
2915
|
+
returnType: extractReturnType(child),
|
|
2916
|
+
isAsync: isAsyncFunc(child),
|
|
2917
|
+
sourceFile
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
if (methods.length > 0) traits.push({
|
|
2921
|
+
name: nameNode.text,
|
|
2922
|
+
methods,
|
|
2923
|
+
sourceFile
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
return traits;
|
|
2927
|
+
}
|
|
2928
|
+
function parseTypeAliases$1(tree, sourceFile) {
|
|
2929
|
+
const aliases = [];
|
|
2930
|
+
for (const node of tree.rootNode.descendantsOfType("type_item")) {
|
|
2931
|
+
if (!isPublic(node)) continue;
|
|
2932
|
+
const nameNode = node.childForFieldName("name");
|
|
2933
|
+
const typeNode = node.childForFieldName("type");
|
|
2934
|
+
if (!nameNode || !typeNode) continue;
|
|
2935
|
+
aliases.push({
|
|
2936
|
+
name: nameNode.text,
|
|
2937
|
+
underlyingType: typeNode.text,
|
|
2938
|
+
sourceFile
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
return aliases;
|
|
2942
|
+
}
|
|
2943
|
+
function parseRustFile(filePath, sdkPath) {
|
|
2944
|
+
const source = readFileSync(filePath, "utf-8");
|
|
2945
|
+
const relPath = relative(sdkPath, filePath);
|
|
2946
|
+
const tree = safeParse$1(rustParser, source);
|
|
2947
|
+
return {
|
|
2948
|
+
structs: parseStructs(tree, relPath),
|
|
2949
|
+
enums: parseEnums$1(tree, relPath),
|
|
2950
|
+
funcs: parseImplBlocks(tree, relPath),
|
|
2951
|
+
traits: parseTraits(tree, relPath),
|
|
2952
|
+
typeAliases: parseTypeAliases$1(tree, relPath)
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
//#endregion
|
|
2956
|
+
//#region src/compat/extractors/rust-surface.ts
|
|
2957
|
+
function rustTypeToString(t) {
|
|
2958
|
+
return t;
|
|
2959
|
+
}
|
|
2960
|
+
/** Strip Result<T, E> to just T. */
|
|
2961
|
+
function unwrapResultType(returnType) {
|
|
2962
|
+
const match = returnType.match(/^Result\s*<\s*(.+)\s*,\s*.+\s*>$/);
|
|
2963
|
+
if (match) return match[1].trim();
|
|
2964
|
+
return returnType;
|
|
2965
|
+
}
|
|
2966
|
+
function buildSurface$3(allStructs, allEnums, allFuncs, allTraits, allTypeAliases) {
|
|
2967
|
+
const classes = {};
|
|
2968
|
+
const interfaces = {};
|
|
2969
|
+
const typeAliases = {};
|
|
2970
|
+
const enums = {};
|
|
2971
|
+
const methodsByReceiver = /* @__PURE__ */ new Map();
|
|
2972
|
+
for (const fn of allFuncs) {
|
|
2973
|
+
if (!fn.receiverType) continue;
|
|
2974
|
+
if (!methodsByReceiver.has(fn.receiverType)) methodsByReceiver.set(fn.receiverType, []);
|
|
2975
|
+
methodsByReceiver.get(fn.receiverType).push(fn);
|
|
2976
|
+
}
|
|
2977
|
+
for (const s of allStructs) {
|
|
2978
|
+
const methods = methodsByReceiver.get(s.name);
|
|
2979
|
+
if (methods && methods.length > 0) {
|
|
2980
|
+
const apiMethods = {};
|
|
2981
|
+
const properties = {};
|
|
2982
|
+
for (const field of s.fields) {
|
|
2983
|
+
const fieldName = field.serdeRename || field.name;
|
|
2984
|
+
properties[fieldName] = {
|
|
2985
|
+
name: fieldName,
|
|
2986
|
+
type: rustTypeToString(field.type),
|
|
2987
|
+
readonly: false
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
for (const fn of methods) {
|
|
2991
|
+
const params = fn.params.map((p) => ({
|
|
2992
|
+
name: p.name,
|
|
2993
|
+
type: rustTypeToString(p.type),
|
|
2994
|
+
optional: p.type.startsWith("Option<")
|
|
2995
|
+
}));
|
|
2996
|
+
const returnType = unwrapResultType(fn.returnType);
|
|
2997
|
+
if (!apiMethods[fn.name]) apiMethods[fn.name] = [];
|
|
2998
|
+
apiMethods[fn.name].push({
|
|
2999
|
+
name: fn.name,
|
|
3000
|
+
params,
|
|
3001
|
+
returnType: rustTypeToString(returnType),
|
|
3002
|
+
async: fn.isAsync
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
classes[s.name] = {
|
|
3006
|
+
name: s.name,
|
|
3007
|
+
sourceFile: s.sourceFile,
|
|
3008
|
+
methods: sortRecord(apiMethods),
|
|
3009
|
+
properties: sortRecord(properties),
|
|
3010
|
+
constructorParams: []
|
|
3011
|
+
};
|
|
3012
|
+
} else {
|
|
3013
|
+
const fields = {};
|
|
3014
|
+
for (const field of s.fields) {
|
|
3015
|
+
const fieldName = field.serdeRename || field.name;
|
|
3016
|
+
fields[fieldName] = {
|
|
3017
|
+
name: fieldName,
|
|
3018
|
+
type: rustTypeToString(field.type),
|
|
3019
|
+
optional: field.optional
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
interfaces[s.name] = {
|
|
3023
|
+
name: s.name,
|
|
3024
|
+
sourceFile: s.sourceFile,
|
|
3025
|
+
fields: sortRecord(fields),
|
|
3026
|
+
extends: []
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
for (const e of allEnums) {
|
|
3031
|
+
const members = {};
|
|
3032
|
+
for (const v of e.variants) members[v.name] = v.serdeRename || v.name;
|
|
3033
|
+
enums[e.name] = {
|
|
3034
|
+
name: e.name,
|
|
3035
|
+
sourceFile: e.sourceFile,
|
|
3036
|
+
members: sortRecord(members)
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
for (const ta of allTypeAliases) typeAliases[ta.name] = {
|
|
3040
|
+
name: ta.name,
|
|
3041
|
+
sourceFile: ta.sourceFile,
|
|
3042
|
+
value: rustTypeToString(ta.underlyingType)
|
|
3043
|
+
};
|
|
3044
|
+
for (const trait of allTraits) {
|
|
3045
|
+
const apiMethods = {};
|
|
3046
|
+
for (const fn of trait.methods) {
|
|
3047
|
+
const params = fn.params.map((p) => ({
|
|
3048
|
+
name: p.name,
|
|
3049
|
+
type: rustTypeToString(p.type),
|
|
3050
|
+
optional: p.type.startsWith("Option<")
|
|
3051
|
+
}));
|
|
3052
|
+
const returnType = unwrapResultType(fn.returnType);
|
|
3053
|
+
if (!apiMethods[fn.name]) apiMethods[fn.name] = [];
|
|
3054
|
+
apiMethods[fn.name].push({
|
|
3055
|
+
name: fn.name,
|
|
3056
|
+
params,
|
|
3057
|
+
returnType: rustTypeToString(returnType),
|
|
3058
|
+
async: fn.isAsync
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
classes[trait.name] = {
|
|
3062
|
+
name: trait.name,
|
|
3063
|
+
sourceFile: trait.sourceFile,
|
|
3064
|
+
methods: sortRecord(apiMethods),
|
|
3065
|
+
properties: {},
|
|
3066
|
+
constructorParams: []
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
const collector = new ExportCollector();
|
|
3070
|
+
for (const [name, cls] of Object.entries(classes)) if (cls.sourceFile) collector.add(cls.sourceFile, name);
|
|
3071
|
+
for (const [name, iface] of Object.entries(interfaces)) if (iface.sourceFile) collector.add(iface.sourceFile, name);
|
|
3072
|
+
for (const [name, ta] of Object.entries(typeAliases)) if (ta.sourceFile) collector.add(ta.sourceFile, name);
|
|
3073
|
+
for (const [name, en] of Object.entries(enums)) if (en.sourceFile) collector.add(en.sourceFile, name);
|
|
3074
|
+
const exports = collector.toRecord();
|
|
3075
|
+
return {
|
|
3076
|
+
classes: sortRecord(classes),
|
|
3077
|
+
interfaces: sortRecord(interfaces),
|
|
3078
|
+
typeAliases: sortRecord(typeAliases),
|
|
3079
|
+
enums: sortRecord(enums),
|
|
3080
|
+
exports: sortRecord(exports)
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
const rustExtractor = {
|
|
3084
|
+
language: "rust",
|
|
3085
|
+
hints: {
|
|
3086
|
+
stripNullable(type) {
|
|
3087
|
+
const match = type.match(/^Option<(.+)>$/);
|
|
3088
|
+
return match ? match[1] : null;
|
|
3089
|
+
},
|
|
3090
|
+
isNullableOnlyDifference(a, b) {
|
|
3091
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
3092
|
+
},
|
|
3093
|
+
isUnionReorder(_a, _b) {
|
|
3094
|
+
return false;
|
|
3095
|
+
},
|
|
3096
|
+
isGenericTypeParam(type) {
|
|
3097
|
+
return /^[A-Z]$/.test(type) || /^T[A-Z][a-zA-Z]*$/.test(type);
|
|
3098
|
+
},
|
|
3099
|
+
isExtractionArtifact(type) {
|
|
3100
|
+
return type === "serde_json::Value" || type === "Box<dyn std::error::Error>";
|
|
3101
|
+
},
|
|
3102
|
+
tolerateCategoryMismatch: false,
|
|
3103
|
+
extractReturnTypeName(returnType) {
|
|
3104
|
+
let inner = returnType;
|
|
3105
|
+
const resultMatch = inner.match(/^Result\s*<\s*(.+)\s*,\s*.+\s*>$/);
|
|
3106
|
+
if (resultMatch) inner = resultMatch[1].trim();
|
|
3107
|
+
const vecMatch = inner.match(/^Vec\s*<\s*(.+)\s*>$/);
|
|
3108
|
+
if (vecMatch) inner = vecMatch[1].trim();
|
|
3109
|
+
const optMatch = inner.match(/^Option\s*<\s*(.+)\s*>$/);
|
|
3110
|
+
if (optMatch) inner = optMatch[1].trim();
|
|
3111
|
+
inner = inner.replace(/^&(?:mut\s+)?(?:'[a-z]+\s+)?/, "");
|
|
3112
|
+
if ([
|
|
3113
|
+
"()",
|
|
3114
|
+
"String",
|
|
3115
|
+
"str",
|
|
3116
|
+
"bool",
|
|
3117
|
+
"i32",
|
|
3118
|
+
"i64",
|
|
3119
|
+
"u32",
|
|
3120
|
+
"u64",
|
|
3121
|
+
"f32",
|
|
3122
|
+
"f64",
|
|
3123
|
+
"usize"
|
|
3124
|
+
].includes(inner)) return null;
|
|
3125
|
+
return inner;
|
|
3126
|
+
},
|
|
3127
|
+
extractParamTypeName(paramType) {
|
|
3128
|
+
let inner = paramType.replace(/^&(?:mut\s+)?(?:'[a-z]+\s+)?/, "");
|
|
3129
|
+
if ([
|
|
3130
|
+
"String",
|
|
3131
|
+
"&str",
|
|
3132
|
+
"str",
|
|
3133
|
+
"bool",
|
|
3134
|
+
"i32",
|
|
3135
|
+
"i64",
|
|
3136
|
+
"u32",
|
|
3137
|
+
"u64",
|
|
3138
|
+
"f32",
|
|
3139
|
+
"f64",
|
|
3140
|
+
"usize"
|
|
3141
|
+
].includes(inner)) return null;
|
|
3142
|
+
return inner;
|
|
3143
|
+
},
|
|
3144
|
+
propertyMatchesClass(propertyName, className) {
|
|
3145
|
+
return propertyName.replace(/_/g, "").toLowerCase() === className.toLowerCase();
|
|
3146
|
+
},
|
|
3147
|
+
derivedModelNames(modelName) {
|
|
3148
|
+
return [`${modelName}Response`];
|
|
3149
|
+
}
|
|
3150
|
+
},
|
|
3151
|
+
async extract(sdkPath) {
|
|
3152
|
+
const rsFiles = walkRustFiles(sdkPath);
|
|
3153
|
+
if (rsFiles.length === 0) throw new ExtractorError(`No .rs files found in ${sdkPath}`, `Ensure the --sdk-path argument points to a Rust project root containing .rs source files.`);
|
|
3154
|
+
const allStructs = [];
|
|
3155
|
+
const allEnums = [];
|
|
3156
|
+
const allFuncs = [];
|
|
3157
|
+
const allTraits = [];
|
|
3158
|
+
const allTypeAliases = [];
|
|
3159
|
+
for (const filePath of rsFiles) {
|
|
3160
|
+
const parsed = parseRustFile(filePath, sdkPath);
|
|
3161
|
+
allStructs.push(...parsed.structs);
|
|
3162
|
+
allEnums.push(...parsed.enums);
|
|
3163
|
+
allFuncs.push(...parsed.funcs);
|
|
3164
|
+
allTraits.push(...parsed.traits);
|
|
3165
|
+
allTypeAliases.push(...parsed.typeAliases);
|
|
3166
|
+
}
|
|
3167
|
+
const { classes, interfaces, typeAliases, enums, exports } = buildSurface$3(allStructs, allEnums, allFuncs, allTraits, allTypeAliases);
|
|
3168
|
+
return {
|
|
3169
|
+
language: "rust",
|
|
3170
|
+
extractedFrom: sdkPath,
|
|
3171
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3172
|
+
classes,
|
|
3173
|
+
interfaces,
|
|
3174
|
+
typeAliases,
|
|
3175
|
+
enums,
|
|
3176
|
+
exports
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
//#endregion
|
|
3181
|
+
//#region src/compat/extractors/kotlin-parser.ts
|
|
3182
|
+
/**
|
|
3183
|
+
* Kotlin source parser — regex-based extraction of classes, data classes,
|
|
3184
|
+
* enum classes, functions, and type aliases from Kotlin source files.
|
|
3185
|
+
*/
|
|
3186
|
+
function walkKotlinFiles(dir) {
|
|
3187
|
+
const results = [];
|
|
3188
|
+
let entries;
|
|
3189
|
+
try {
|
|
3190
|
+
entries = readdirSync(dir);
|
|
3191
|
+
} catch {
|
|
3192
|
+
return results;
|
|
3193
|
+
}
|
|
3194
|
+
for (const entry of entries) {
|
|
3195
|
+
const fullPath = join(dir, entry);
|
|
3196
|
+
let stat;
|
|
3197
|
+
try {
|
|
3198
|
+
stat = statSync(fullPath);
|
|
3199
|
+
} catch {
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
if (stat.isDirectory()) {
|
|
3203
|
+
if (entry.startsWith(".") || entry === "build" || entry === "test" || entry === "androidTest") continue;
|
|
3204
|
+
results.push(...walkKotlinFiles(fullPath));
|
|
3205
|
+
} else if (entry.endsWith(".kt") && !entry.endsWith("Test.kt")) results.push(fullPath);
|
|
3206
|
+
}
|
|
3207
|
+
return results.sort();
|
|
3208
|
+
}
|
|
3209
|
+
/** Extract the package name from a Kotlin source file. */
|
|
3210
|
+
function extractPackageName(source) {
|
|
3211
|
+
const match = source.match(/^package\s+([\w.]+)/m);
|
|
3212
|
+
return match ? match[1] : "unknown";
|
|
3213
|
+
}
|
|
3214
|
+
/** Strip single-line and block comments from source to avoid false matches. */
|
|
3215
|
+
function stripComments$1(source) {
|
|
3216
|
+
let result = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3217
|
+
result = result.replace(/\/\/[^\n]*/g, "");
|
|
3218
|
+
return result;
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Extract the @JsonProperty("name") annotation value before a parameter.
|
|
3222
|
+
* Looks for the pattern immediately preceding the parameter declaration.
|
|
3223
|
+
*/
|
|
3224
|
+
function extractJsonPropertyName$1(text) {
|
|
3225
|
+
const match = text.match(/@JsonProperty\("([^"]+)"\)/);
|
|
3226
|
+
return match ? match[1] : null;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Parse constructor parameters from a data class or class declaration.
|
|
3230
|
+
* Handles multi-line parameter lists with annotations.
|
|
3231
|
+
*/
|
|
3232
|
+
function parseConstructorParams(paramsBlock) {
|
|
3233
|
+
const fields = [];
|
|
3234
|
+
const params = splitTopLevelCommas(paramsBlock);
|
|
3235
|
+
for (const param of params) {
|
|
3236
|
+
const trimmed = param.trim();
|
|
3237
|
+
if (!trimmed) continue;
|
|
3238
|
+
const jsonName = extractJsonPropertyName$1(trimmed);
|
|
3239
|
+
const paramMatch = trimmed.match(/(?:(?:override|private|protected)\s+)?(?:val|var)\s+(\w+)\s*:\s*([^=,]+?)(?:\s*=\s*(.+))?$/m);
|
|
3240
|
+
if (!paramMatch) continue;
|
|
3241
|
+
const name = paramMatch[1];
|
|
3242
|
+
const rawType = paramMatch[2].trim();
|
|
3243
|
+
const hasDefault = paramMatch[3] !== void 0;
|
|
3244
|
+
const isNullable = rawType.endsWith("?");
|
|
3245
|
+
const type = rawType;
|
|
3246
|
+
fields.push({
|
|
3247
|
+
name,
|
|
3248
|
+
type,
|
|
3249
|
+
jsonName: jsonName || void 0,
|
|
3250
|
+
optional: isNullable || hasDefault
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
return fields;
|
|
3254
|
+
}
|
|
3255
|
+
/** Split a string by top-level commas, ignoring commas inside angle brackets, parens, or strings. */
|
|
3256
|
+
function splitTopLevelCommas(text) {
|
|
3257
|
+
const result = [];
|
|
3258
|
+
let depth = 0;
|
|
3259
|
+
let current = "";
|
|
3260
|
+
let inString = false;
|
|
3261
|
+
let stringChar = "";
|
|
3262
|
+
for (let i = 0; i < text.length; i++) {
|
|
3263
|
+
const ch = text[i];
|
|
3264
|
+
if (inString) {
|
|
3265
|
+
current += ch;
|
|
3266
|
+
if (ch === stringChar && text[i - 1] !== "\\") inString = false;
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
if (ch === "\"" || ch === "'") {
|
|
3270
|
+
inString = true;
|
|
3271
|
+
stringChar = ch;
|
|
3272
|
+
current += ch;
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
if (ch === "<" || ch === "(" || ch === "[") {
|
|
3276
|
+
depth++;
|
|
3277
|
+
current += ch;
|
|
3278
|
+
} else if (ch === ">" || ch === ")" || ch === "]") {
|
|
3279
|
+
depth--;
|
|
3280
|
+
current += ch;
|
|
3281
|
+
} else if (ch === "," && depth === 0) {
|
|
3282
|
+
result.push(current);
|
|
3283
|
+
current = "";
|
|
3284
|
+
} else current += ch;
|
|
3285
|
+
}
|
|
3286
|
+
if (current.trim()) result.push(current);
|
|
3287
|
+
return result;
|
|
3288
|
+
}
|
|
3289
|
+
function parseEnumClass(source, sourceFile, packageName) {
|
|
3290
|
+
const enums = [];
|
|
3291
|
+
const enumRegex = /enum\s+class\s+(\w+)(?:\([^)]*\))?\s*\{([^}]*)\}/g;
|
|
3292
|
+
let match;
|
|
3293
|
+
while ((match = enumRegex.exec(source)) !== null) {
|
|
3294
|
+
const name = match[1];
|
|
3295
|
+
const body = match[2];
|
|
3296
|
+
const members = {};
|
|
3297
|
+
const memberRegex = /(\w+)\s*(?:\("([^"]*)"\))?/g;
|
|
3298
|
+
let memberMatch;
|
|
3299
|
+
while ((memberMatch = memberRegex.exec(body)) !== null) {
|
|
3300
|
+
const memberName = memberMatch[1];
|
|
3301
|
+
if ([
|
|
3302
|
+
"val",
|
|
3303
|
+
"var",
|
|
3304
|
+
"fun",
|
|
3305
|
+
"override",
|
|
3306
|
+
"companion",
|
|
3307
|
+
"object"
|
|
3308
|
+
].includes(memberName)) continue;
|
|
3309
|
+
members[memberName] = memberMatch[2] !== void 0 ? memberMatch[2] : memberName;
|
|
3310
|
+
}
|
|
3311
|
+
if (Object.keys(members).length > 0) enums.push({
|
|
3312
|
+
name,
|
|
3313
|
+
members,
|
|
3314
|
+
sourceFile,
|
|
3315
|
+
packageName
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
return enums;
|
|
3319
|
+
}
|
|
3320
|
+
function parseDataClasses(source, sourceFile, packageName) {
|
|
3321
|
+
const dataClasses = [];
|
|
3322
|
+
const dataClassStartRegex = /data\s+class\s+(\w+)\s*(?:@\w+(?:\([^)]*\))?\s+)?(?:constructor\s*)?\(/g;
|
|
3323
|
+
let match;
|
|
3324
|
+
while ((match = dataClassStartRegex.exec(source)) !== null) {
|
|
3325
|
+
const name = match[1];
|
|
3326
|
+
const startIdx = match.index + match[0].length;
|
|
3327
|
+
let depth = 1;
|
|
3328
|
+
let idx = startIdx;
|
|
3329
|
+
while (idx < source.length && depth > 0) {
|
|
3330
|
+
if (source[idx] === "(") depth++;
|
|
3331
|
+
else if (source[idx] === ")") depth--;
|
|
3332
|
+
idx++;
|
|
3333
|
+
}
|
|
3334
|
+
const fields = parseConstructorParams(source.slice(startIdx, idx - 1));
|
|
3335
|
+
dataClasses.push({
|
|
3336
|
+
name,
|
|
3337
|
+
fields,
|
|
3338
|
+
sourceFile,
|
|
3339
|
+
packageName
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
return dataClasses;
|
|
3343
|
+
}
|
|
3344
|
+
function parseClasses$1(source, sourceFile, packageName) {
|
|
3345
|
+
const classes = [];
|
|
3346
|
+
const classStartRegex = /(?<!data\s)(?<!enum\s)(?<!abstract\s)(?<!sealed\s)(?:open\s+)?class\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?::\s*[^{]+)?\s*\{/g;
|
|
3347
|
+
let match;
|
|
3348
|
+
while ((match = classStartRegex.exec(source)) !== null) {
|
|
3349
|
+
const name = match[1];
|
|
3350
|
+
const constructorBlock = match[2] || "";
|
|
3351
|
+
const braceStart = source.indexOf("{", match.index + match[0].length - 1);
|
|
3352
|
+
let depth = 1;
|
|
3353
|
+
let idx = braceStart + 1;
|
|
3354
|
+
while (idx < source.length && depth > 0) {
|
|
3355
|
+
if (source[idx] === "{") depth++;
|
|
3356
|
+
else if (source[idx] === "}") depth--;
|
|
3357
|
+
idx++;
|
|
3358
|
+
}
|
|
3359
|
+
const classBody = source.slice(braceStart + 1, idx - 1);
|
|
3360
|
+
const constructorParams = [];
|
|
3361
|
+
if (constructorBlock.trim()) {
|
|
3362
|
+
const params = splitTopLevelCommas(constructorBlock);
|
|
3363
|
+
for (const param of params) {
|
|
3364
|
+
const paramMatch = param.trim().match(/(?:(?:private|protected)\s+)?(?:val|var)\s+(\w+)\s*:\s*(.+)/);
|
|
3365
|
+
if (paramMatch) constructorParams.push({
|
|
3366
|
+
name: paramMatch[1],
|
|
3367
|
+
type: paramMatch[2].trim()
|
|
3368
|
+
});
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
const methods = parseMethods$1(classBody);
|
|
3372
|
+
if (methods.length > 0) classes.push({
|
|
3373
|
+
name,
|
|
3374
|
+
constructorParams,
|
|
3375
|
+
methods,
|
|
3376
|
+
sourceFile,
|
|
3377
|
+
packageName
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
return classes;
|
|
3381
|
+
}
|
|
3382
|
+
/** Parse method declarations from a class body. */
|
|
3383
|
+
function parseMethods$1(classBody) {
|
|
3384
|
+
const methods = [];
|
|
3385
|
+
const methodRegex = /(private|protected|internal|public)?\s*(?:suspend\s+)?fun\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\s{]+))?/g;
|
|
3386
|
+
let match;
|
|
3387
|
+
while ((match = methodRegex.exec(classBody)) !== null) {
|
|
3388
|
+
const visibility = match[1] || "public";
|
|
3389
|
+
const name = match[2];
|
|
3390
|
+
const paramsStr = match[3];
|
|
3391
|
+
const returnType = match[4] || "Unit";
|
|
3392
|
+
if (visibility === "private" || visibility === "protected") continue;
|
|
3393
|
+
const params = [];
|
|
3394
|
+
if (paramsStr.trim()) {
|
|
3395
|
+
const paramParts = splitTopLevelCommas(paramsStr);
|
|
3396
|
+
for (const part of paramParts) {
|
|
3397
|
+
const paramMatch = part.trim().match(/(\w+)\s*:\s*([^=]+?)(?:\s*=\s*(.+))?$/);
|
|
3398
|
+
if (paramMatch) {
|
|
3399
|
+
const paramType = paramMatch[2].trim();
|
|
3400
|
+
const hasDefault = paramMatch[3] !== void 0;
|
|
3401
|
+
params.push({
|
|
3402
|
+
name: paramMatch[1],
|
|
3403
|
+
type: paramType,
|
|
3404
|
+
optional: paramType.endsWith("?") || hasDefault
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
methods.push({
|
|
3410
|
+
name,
|
|
3411
|
+
params,
|
|
3412
|
+
returnType,
|
|
3413
|
+
visibility
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
return methods;
|
|
3417
|
+
}
|
|
3418
|
+
function parseTypeAliases(source, sourceFile, packageName) {
|
|
3419
|
+
const aliases = [];
|
|
3420
|
+
const aliasRegex = /typealias\s+(\w+)\s*=\s*([^\n]+)/g;
|
|
3421
|
+
let match;
|
|
3422
|
+
while ((match = aliasRegex.exec(source)) !== null) aliases.push({
|
|
3423
|
+
name: match[1],
|
|
3424
|
+
value: match[2].trim(),
|
|
3425
|
+
sourceFile,
|
|
3426
|
+
packageName
|
|
3427
|
+
});
|
|
3428
|
+
return aliases;
|
|
3429
|
+
}
|
|
3430
|
+
/** Parse a single Kotlin source file and return all extracted symbols. */
|
|
3431
|
+
function parseKotlinFile(filePath, sdkPath) {
|
|
3432
|
+
const source = readFileSync(filePath, "utf-8");
|
|
3433
|
+
const relPath = relative(sdkPath, filePath);
|
|
3434
|
+
const cleaned = stripComments$1(source);
|
|
3435
|
+
const packageName = extractPackageName(cleaned);
|
|
3436
|
+
return {
|
|
3437
|
+
dataClasses: parseDataClasses(cleaned, relPath, packageName),
|
|
3438
|
+
classes: parseClasses$1(cleaned, relPath, packageName),
|
|
3439
|
+
enums: parseEnumClass(cleaned, relPath, packageName),
|
|
3440
|
+
typeAliases: parseTypeAliases(cleaned, relPath, packageName)
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
//#endregion
|
|
3444
|
+
//#region src/compat/extractors/kotlin-surface.ts
|
|
3445
|
+
function buildSurface$2(allDataClasses, allClasses, allEnums, allTypeAliases) {
|
|
3446
|
+
const classes = {};
|
|
3447
|
+
const interfaces = {};
|
|
3448
|
+
const typeAliases = {};
|
|
3449
|
+
const enums = {};
|
|
3450
|
+
const collector = new ExportCollector();
|
|
3451
|
+
for (const dc of allDataClasses) {
|
|
3452
|
+
const fields = {};
|
|
3453
|
+
for (const field of dc.fields) {
|
|
3454
|
+
const fieldName = field.jsonName || field.name;
|
|
3455
|
+
fields[fieldName] = {
|
|
3456
|
+
name: fieldName,
|
|
3457
|
+
type: field.type,
|
|
3458
|
+
optional: field.optional
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
interfaces[dc.name] = {
|
|
3462
|
+
name: dc.name,
|
|
3463
|
+
sourceFile: dc.sourceFile,
|
|
3464
|
+
fields: sortRecord(fields),
|
|
3465
|
+
extends: []
|
|
3466
|
+
};
|
|
3467
|
+
collector.add(dc.sourceFile, dc.name);
|
|
3468
|
+
}
|
|
3469
|
+
for (const cls of allClasses) {
|
|
3470
|
+
const methods = {};
|
|
3471
|
+
const properties = {};
|
|
3472
|
+
for (const method of cls.methods) {
|
|
3473
|
+
const params = method.params.map((p) => ({
|
|
3474
|
+
name: p.name,
|
|
3475
|
+
type: p.type,
|
|
3476
|
+
optional: p.optional
|
|
3477
|
+
}));
|
|
3478
|
+
if (!methods[method.name]) methods[method.name] = [];
|
|
3479
|
+
methods[method.name].push({
|
|
3480
|
+
name: method.name,
|
|
3481
|
+
params,
|
|
3482
|
+
returnType: method.returnType,
|
|
3483
|
+
async: false
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
const constructorParams = cls.constructorParams.map((p) => ({
|
|
3487
|
+
name: p.name,
|
|
3488
|
+
type: p.type,
|
|
3489
|
+
optional: false
|
|
3490
|
+
}));
|
|
3491
|
+
classes[cls.name] = {
|
|
3492
|
+
name: cls.name,
|
|
3493
|
+
sourceFile: cls.sourceFile,
|
|
3494
|
+
methods: sortRecord(methods),
|
|
3495
|
+
properties: sortRecord(properties),
|
|
3496
|
+
constructorParams
|
|
3497
|
+
};
|
|
3498
|
+
collector.add(cls.sourceFile, cls.name);
|
|
3499
|
+
}
|
|
3500
|
+
for (const en of allEnums) {
|
|
3501
|
+
enums[en.name] = {
|
|
3502
|
+
name: en.name,
|
|
3503
|
+
sourceFile: en.sourceFile,
|
|
3504
|
+
members: sortRecord(en.members)
|
|
3505
|
+
};
|
|
3506
|
+
collector.add(en.sourceFile, en.name);
|
|
3507
|
+
}
|
|
3508
|
+
for (const ta of allTypeAliases) {
|
|
3509
|
+
typeAliases[ta.name] = {
|
|
3510
|
+
name: ta.name,
|
|
3511
|
+
sourceFile: ta.sourceFile,
|
|
3512
|
+
value: ta.value
|
|
3513
|
+
};
|
|
3514
|
+
collector.add(ta.sourceFile, ta.name);
|
|
3515
|
+
}
|
|
3516
|
+
const exports = collector.toRecord();
|
|
3517
|
+
return {
|
|
3518
|
+
classes: sortRecord(classes),
|
|
3519
|
+
interfaces: sortRecord(interfaces),
|
|
3520
|
+
typeAliases: sortRecord(typeAliases),
|
|
3521
|
+
enums: sortRecord(enums),
|
|
3522
|
+
exports: sortRecord(exports)
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
const kotlinExtractor = {
|
|
3526
|
+
language: "kotlin",
|
|
3527
|
+
hints: {
|
|
3528
|
+
stripNullable(type) {
|
|
3529
|
+
if (type.endsWith("?")) return type.slice(0, -1);
|
|
3530
|
+
return null;
|
|
3531
|
+
},
|
|
3532
|
+
isNullableOnlyDifference(a, b) {
|
|
3533
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
3534
|
+
},
|
|
3535
|
+
isUnionReorder(_a, _b) {
|
|
3536
|
+
return false;
|
|
3537
|
+
},
|
|
3538
|
+
isGenericTypeParam(type) {
|
|
3539
|
+
return /^[A-Z]$/.test(type) || /^T[A-Z][a-zA-Z]*$/.test(type);
|
|
3540
|
+
},
|
|
3541
|
+
isExtractionArtifact(type) {
|
|
3542
|
+
return type === "Any" || type === "Any?";
|
|
3543
|
+
},
|
|
3544
|
+
tolerateCategoryMismatch: true,
|
|
3545
|
+
extractReturnTypeName(returnType) {
|
|
3546
|
+
let inner = returnType;
|
|
3547
|
+
if (inner.endsWith("?")) inner = inner.slice(0, -1);
|
|
3548
|
+
const listMatch = inner.match(/^(?:List|MutableList|ArrayList)<(.+)>$/);
|
|
3549
|
+
if (listMatch) inner = listMatch[1];
|
|
3550
|
+
const paginatedMatch = inner.match(/^PaginatedList<(.+)>$/);
|
|
3551
|
+
if (paginatedMatch) inner = paginatedMatch[1];
|
|
3552
|
+
if ([
|
|
3553
|
+
"String",
|
|
3554
|
+
"Int",
|
|
3555
|
+
"Long",
|
|
3556
|
+
"Float",
|
|
3557
|
+
"Double",
|
|
3558
|
+
"Boolean",
|
|
3559
|
+
"Unit",
|
|
3560
|
+
"Void"
|
|
3561
|
+
].includes(inner)) return null;
|
|
3562
|
+
return inner;
|
|
3563
|
+
},
|
|
3564
|
+
extractParamTypeName(paramType) {
|
|
3565
|
+
let inner = paramType;
|
|
3566
|
+
if (inner.endsWith("?")) inner = inner.slice(0, -1);
|
|
3567
|
+
if ([
|
|
3568
|
+
"String",
|
|
3569
|
+
"Int",
|
|
3570
|
+
"Long",
|
|
3571
|
+
"Float",
|
|
3572
|
+
"Double",
|
|
3573
|
+
"Boolean"
|
|
3574
|
+
].includes(inner)) return null;
|
|
3575
|
+
return inner;
|
|
3576
|
+
},
|
|
3577
|
+
propertyMatchesClass(propertyName, className) {
|
|
3578
|
+
return propertyName.toLowerCase() === className.toLowerCase();
|
|
3579
|
+
},
|
|
3580
|
+
derivedModelNames(modelName) {
|
|
3581
|
+
return [`${modelName}Response`];
|
|
3582
|
+
}
|
|
3583
|
+
},
|
|
3584
|
+
async extract(sdkPath) {
|
|
3585
|
+
const ktFiles = walkKotlinFiles(sdkPath);
|
|
3586
|
+
if (ktFiles.length === 0) throw new ExtractorError(`No .kt files found in ${sdkPath}`, `Ensure the --sdk-path argument points to a Kotlin project root containing .kt source files.`);
|
|
3587
|
+
const allDataClasses = [];
|
|
3588
|
+
const allClasses = [];
|
|
3589
|
+
const allEnums = [];
|
|
3590
|
+
const allTypeAliases = [];
|
|
3591
|
+
for (const filePath of ktFiles) {
|
|
3592
|
+
const parsed = parseKotlinFile(filePath, sdkPath);
|
|
3593
|
+
allDataClasses.push(...parsed.dataClasses);
|
|
3594
|
+
allClasses.push(...parsed.classes);
|
|
3595
|
+
allEnums.push(...parsed.enums);
|
|
3596
|
+
allTypeAliases.push(...parsed.typeAliases);
|
|
3597
|
+
}
|
|
3598
|
+
const { classes, interfaces, typeAliases, enums, exports } = buildSurface$2(allDataClasses, allClasses, allEnums, allTypeAliases);
|
|
3599
|
+
return {
|
|
3600
|
+
language: "kotlin",
|
|
3601
|
+
extractedFrom: sdkPath,
|
|
3602
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3603
|
+
classes,
|
|
3604
|
+
interfaces,
|
|
3605
|
+
typeAliases,
|
|
3606
|
+
enums,
|
|
3607
|
+
exports
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
//#endregion
|
|
3612
|
+
//#region src/compat/extractors/dotnet-parser.ts
|
|
3613
|
+
/**
|
|
3614
|
+
* C# (.NET) source parser — regex-based extraction of classes, enums,
|
|
3615
|
+
* properties, and methods from C# source files.
|
|
3616
|
+
*/
|
|
3617
|
+
function walkCSharpFiles(dir) {
|
|
3618
|
+
const results = [];
|
|
3619
|
+
let entries;
|
|
3620
|
+
try {
|
|
3621
|
+
entries = readdirSync(dir);
|
|
3622
|
+
} catch {
|
|
3623
|
+
return results;
|
|
3624
|
+
}
|
|
3625
|
+
for (const entry of entries) {
|
|
3626
|
+
const fullPath = join(dir, entry);
|
|
3627
|
+
let stat;
|
|
3628
|
+
try {
|
|
3629
|
+
stat = statSync(fullPath);
|
|
3630
|
+
} catch {
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
if (stat.isDirectory()) {
|
|
3634
|
+
if (entry.startsWith(".") || entry === "bin" || entry === "obj" || entry === "test" || entry === "tests") continue;
|
|
3635
|
+
results.push(...walkCSharpFiles(fullPath));
|
|
3636
|
+
} else if (entry.endsWith(".cs") && !entry.endsWith("Test.cs") && !entry.endsWith("Tests.cs")) results.push(fullPath);
|
|
3637
|
+
}
|
|
3638
|
+
return results.sort();
|
|
3639
|
+
}
|
|
3640
|
+
/** Extract the namespace from a C# source file. */
|
|
3641
|
+
function extractNamespace(source) {
|
|
3642
|
+
const fileScopedMatch = source.match(/^namespace\s+([\w.]+)\s*;/m);
|
|
3643
|
+
if (fileScopedMatch) return fileScopedMatch[1];
|
|
3644
|
+
const blockMatch = source.match(/^namespace\s+([\w.]+)\s*\{/m);
|
|
3645
|
+
if (blockMatch) return blockMatch[1];
|
|
3646
|
+
return "unknown";
|
|
3647
|
+
}
|
|
3648
|
+
/** Strip single-line and block comments from source. */
|
|
3649
|
+
function stripComments(source) {
|
|
3650
|
+
let result = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3651
|
+
result = result.replace(/\/\/[^\n]*/g, "");
|
|
3652
|
+
return result;
|
|
3653
|
+
}
|
|
3654
|
+
/** Extract [JsonProperty("name")] value from text preceding a property. */
|
|
3655
|
+
function extractJsonPropertyName(text) {
|
|
3656
|
+
const match = text.match(/\[JsonProperty\("([^"]+)"\)\]/);
|
|
3657
|
+
return match ? match[1] : null;
|
|
3658
|
+
}
|
|
3659
|
+
/** Extract [EnumMember(Value = "name")] value from text. */
|
|
3660
|
+
function extractEnumMemberValue(text) {
|
|
3661
|
+
const match = text.match(/\[EnumMember\(Value\s*=\s*"([^"]+)"\)\]/);
|
|
3662
|
+
return match ? match[1] : null;
|
|
3663
|
+
}
|
|
3664
|
+
function parseEnums(source, sourceFile) {
|
|
3665
|
+
const enums = [];
|
|
3666
|
+
const ns = extractNamespace(source);
|
|
3667
|
+
const enumRegex = /public\s+enum\s+(\w+)\s*\{([^}]*)\}/g;
|
|
3668
|
+
let match;
|
|
3669
|
+
while ((match = enumRegex.exec(source)) !== null) {
|
|
3670
|
+
const name = match[1];
|
|
3671
|
+
const body = match[2];
|
|
3672
|
+
const members = {};
|
|
3673
|
+
const lines = body.split(",");
|
|
3674
|
+
for (const line of lines) {
|
|
3675
|
+
const trimmed = line.trim();
|
|
3676
|
+
if (!trimmed) continue;
|
|
3677
|
+
const enumValue = extractEnumMemberValue(trimmed);
|
|
3678
|
+
const memberNameMatch = trimmed.match(/(\w+)\s*$/);
|
|
3679
|
+
if (memberNameMatch) {
|
|
3680
|
+
const memberName = memberNameMatch[1];
|
|
3681
|
+
members[memberName] = enumValue || memberName;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
if (Object.keys(members).length > 0) enums.push({
|
|
3685
|
+
name,
|
|
3686
|
+
namespace: ns,
|
|
3687
|
+
members,
|
|
3688
|
+
sourceFile
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
return enums;
|
|
3692
|
+
}
|
|
3693
|
+
function parseClasses(source, sourceFile) {
|
|
3694
|
+
const classes = [];
|
|
3695
|
+
const ns = extractNamespace(source);
|
|
3696
|
+
const classStartRegex = /public\s+class\s+(\w+)(?:\s*:\s*[\w.,\s<>]+)?\s*\{/g;
|
|
3697
|
+
let match;
|
|
3698
|
+
while ((match = classStartRegex.exec(source)) !== null) {
|
|
3699
|
+
const name = match[1];
|
|
3700
|
+
const braceStart = source.indexOf("{", match.index + match[0].length - 1);
|
|
3701
|
+
let depth = 1;
|
|
3702
|
+
let idx = braceStart + 1;
|
|
3703
|
+
while (idx < source.length && depth > 0) {
|
|
3704
|
+
if (source[idx] === "{") depth++;
|
|
3705
|
+
else if (source[idx] === "}") depth--;
|
|
3706
|
+
idx++;
|
|
3707
|
+
}
|
|
3708
|
+
const classBody = source.slice(braceStart + 1, idx - 1);
|
|
3709
|
+
const properties = parseProperties(classBody);
|
|
3710
|
+
const methods = parseMethods(classBody);
|
|
3711
|
+
const constructorParams = parseConstructor(classBody, name);
|
|
3712
|
+
const isService = methods.length > 0;
|
|
3713
|
+
classes.push({
|
|
3714
|
+
name,
|
|
3715
|
+
namespace: ns,
|
|
3716
|
+
properties,
|
|
3717
|
+
methods,
|
|
3718
|
+
constructorParams,
|
|
3719
|
+
isService,
|
|
3720
|
+
sourceFile
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
return classes;
|
|
3724
|
+
}
|
|
3725
|
+
/** Parse properties from a class body. */
|
|
3726
|
+
function parseProperties(classBody) {
|
|
3727
|
+
const properties = [];
|
|
3728
|
+
const lines = classBody.split("\n");
|
|
3729
|
+
let pendingJsonName = null;
|
|
3730
|
+
for (const line of lines) {
|
|
3731
|
+
const trimmed = line.trim();
|
|
3732
|
+
const jsonPropMatch = extractJsonPropertyName(trimmed);
|
|
3733
|
+
if (jsonPropMatch) {
|
|
3734
|
+
pendingJsonName = jsonPropMatch;
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
const propMatch = trimmed.match(/public\s+(\S+(?:<[^>]+>)?(?:\?)?)\s+(\w+)\s*\{\s*get;\s*(set;\s*)?\}/);
|
|
3738
|
+
if (propMatch) {
|
|
3739
|
+
const type = propMatch[1];
|
|
3740
|
+
const name = propMatch[2];
|
|
3741
|
+
const hasSetter = !!propMatch[3];
|
|
3742
|
+
properties.push({
|
|
3743
|
+
name,
|
|
3744
|
+
type,
|
|
3745
|
+
jsonName: pendingJsonName || void 0,
|
|
3746
|
+
readonly: !hasSetter
|
|
3747
|
+
});
|
|
3748
|
+
pendingJsonName = null;
|
|
3749
|
+
continue;
|
|
3750
|
+
}
|
|
3751
|
+
if (!trimmed.startsWith("[") && trimmed.length > 0) pendingJsonName = null;
|
|
3752
|
+
}
|
|
3753
|
+
return properties;
|
|
3754
|
+
}
|
|
3755
|
+
/** Parse methods from a class body. */
|
|
3756
|
+
function parseMethods(classBody) {
|
|
3757
|
+
const methods = [];
|
|
3758
|
+
const methodRegex = /(private|protected|internal|public)\s+(?:(static)\s+)?(?:(async)\s+)?(\S+(?:<[^>]+>)?)\s+(\w+)\s*\(([^)]*)\)/g;
|
|
3759
|
+
let match;
|
|
3760
|
+
while ((match = methodRegex.exec(classBody)) !== null) {
|
|
3761
|
+
const visibility = match[1];
|
|
3762
|
+
const isAsync = match[3] === "async";
|
|
3763
|
+
const returnType = match[4];
|
|
3764
|
+
const name = match[5];
|
|
3765
|
+
const paramsStr = match[6];
|
|
3766
|
+
if (visibility === "private" || visibility === "protected") continue;
|
|
3767
|
+
const params = [];
|
|
3768
|
+
if (paramsStr.trim()) {
|
|
3769
|
+
const paramParts = paramsStr.split(",");
|
|
3770
|
+
for (const part of paramParts) {
|
|
3771
|
+
const paramMatch = part.trim().match(/(\S+(?:<[^>]+>)?(?:\?)?)\s+(\w+)(?:\s*=\s*(.+))?/);
|
|
3772
|
+
if (paramMatch) {
|
|
3773
|
+
const paramType = paramMatch[1];
|
|
3774
|
+
const paramName = paramMatch[2];
|
|
3775
|
+
const hasDefault = paramMatch[3] !== void 0;
|
|
3776
|
+
params.push({
|
|
3777
|
+
name: paramName,
|
|
3778
|
+
type: paramType,
|
|
3779
|
+
optional: paramType.endsWith("?") || hasDefault
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
methods.push({
|
|
3785
|
+
name,
|
|
3786
|
+
params,
|
|
3787
|
+
returnType,
|
|
3788
|
+
visibility,
|
|
3789
|
+
isAsync
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
return methods;
|
|
3793
|
+
}
|
|
3794
|
+
/** Parse constructor parameters. */
|
|
3795
|
+
function parseConstructor(classBody, className) {
|
|
3796
|
+
const params = [];
|
|
3797
|
+
const ctorPrefix = `public ${className}`;
|
|
3798
|
+
const ctorIdx = classBody.indexOf(ctorPrefix);
|
|
3799
|
+
if (ctorIdx < 0) return params;
|
|
3800
|
+
const match = classBody.slice(ctorIdx + ctorPrefix.length).match(/^\s*\(([^)]*)\)/);
|
|
3801
|
+
if (!match) return params;
|
|
3802
|
+
const paramsStr = match[1];
|
|
3803
|
+
if (!paramsStr.trim()) return params;
|
|
3804
|
+
const paramParts = paramsStr.split(",");
|
|
3805
|
+
for (const part of paramParts) {
|
|
3806
|
+
const paramMatch = part.trim().match(/(\S+(?:<[^>]+>)?)\s+(\w+)/);
|
|
3807
|
+
if (paramMatch) params.push({
|
|
3808
|
+
name: paramMatch[2],
|
|
3809
|
+
type: paramMatch[1]
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
return params;
|
|
3813
|
+
}
|
|
3814
|
+
/** Parse a single C# source file and return all extracted symbols. */
|
|
3815
|
+
function parseCSharpFile(filePath, sdkPath) {
|
|
3816
|
+
const source = readFileSync(filePath, "utf-8");
|
|
3817
|
+
const relPath = relative(sdkPath, filePath);
|
|
3818
|
+
const cleaned = stripComments(source);
|
|
3819
|
+
return {
|
|
3820
|
+
classes: parseClasses(cleaned, relPath),
|
|
3821
|
+
enums: parseEnums(cleaned, relPath)
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
//#endregion
|
|
3825
|
+
//#region src/compat/extractors/dotnet-surface.ts
|
|
3826
|
+
function buildSurface$1(allClasses, allEnums, _hints) {
|
|
3827
|
+
const classes = {};
|
|
3828
|
+
const interfaces = {};
|
|
3829
|
+
const enums = {};
|
|
3830
|
+
const collector = new ExportCollector();
|
|
3831
|
+
for (const cls of allClasses) {
|
|
3832
|
+
if (cls.isService) {
|
|
3833
|
+
const methods = {};
|
|
3834
|
+
const properties = {};
|
|
3835
|
+
for (const method of cls.methods) {
|
|
3836
|
+
const params = method.params.map((p) => ({
|
|
3837
|
+
name: p.name,
|
|
3838
|
+
type: p.type,
|
|
3839
|
+
optional: p.optional
|
|
3840
|
+
}));
|
|
3841
|
+
if (!methods[method.name]) methods[method.name] = [];
|
|
3842
|
+
methods[method.name].push({
|
|
3843
|
+
name: method.name,
|
|
3844
|
+
params,
|
|
3845
|
+
returnType: method.returnType,
|
|
3846
|
+
async: method.isAsync
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
for (const prop of cls.properties) {
|
|
3850
|
+
const propName = prop.jsonName || prop.name;
|
|
3851
|
+
properties[propName] = {
|
|
3852
|
+
name: propName,
|
|
3853
|
+
type: prop.type,
|
|
3854
|
+
readonly: prop.readonly
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
const constructorParams = cls.constructorParams.map((p) => ({
|
|
3858
|
+
name: p.name,
|
|
3859
|
+
type: p.type,
|
|
3860
|
+
optional: false
|
|
3861
|
+
}));
|
|
3862
|
+
classes[cls.name] = {
|
|
3863
|
+
name: cls.name,
|
|
3864
|
+
sourceFile: cls.sourceFile,
|
|
3865
|
+
methods: sortRecord(methods),
|
|
3866
|
+
properties: sortRecord(properties),
|
|
3867
|
+
constructorParams
|
|
3868
|
+
};
|
|
3869
|
+
} else {
|
|
3870
|
+
const fields = {};
|
|
3871
|
+
for (const prop of cls.properties) {
|
|
3872
|
+
const fieldName = prop.jsonName || prop.name;
|
|
3873
|
+
const isNullable = prop.type.endsWith("?");
|
|
3874
|
+
fields[fieldName] = {
|
|
3875
|
+
name: fieldName,
|
|
3876
|
+
type: prop.type,
|
|
3877
|
+
optional: isNullable
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
interfaces[cls.name] = {
|
|
3881
|
+
name: cls.name,
|
|
3882
|
+
sourceFile: cls.sourceFile,
|
|
3883
|
+
fields: sortRecord(fields),
|
|
3884
|
+
extends: []
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
collector.add(cls.sourceFile, cls.name);
|
|
3888
|
+
}
|
|
3889
|
+
for (const en of allEnums) {
|
|
3890
|
+
enums[en.name] = {
|
|
3891
|
+
name: en.name,
|
|
3892
|
+
sourceFile: en.sourceFile,
|
|
3893
|
+
members: sortRecord(en.members)
|
|
3894
|
+
};
|
|
3895
|
+
collector.add(en.sourceFile, en.name);
|
|
3896
|
+
}
|
|
3897
|
+
const exports = collector.toRecord();
|
|
3898
|
+
return {
|
|
3899
|
+
classes: sortRecord(classes),
|
|
3900
|
+
interfaces: sortRecord(interfaces),
|
|
3901
|
+
enums: sortRecord(enums),
|
|
3902
|
+
exports: sortRecord(exports)
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
//#endregion
|
|
3906
|
+
//#region src/compat/extractors/dotnet.ts
|
|
3907
|
+
/**
|
|
3908
|
+
* DotNet (C#) API surface extractor.
|
|
3909
|
+
*
|
|
3910
|
+
* Walks `.cs` files (excluding test, bin, and obj directories), parses
|
|
3911
|
+
* classes, enums, properties, and methods, then builds an `ApiSurface`.
|
|
3912
|
+
*
|
|
3913
|
+
* Parser: `dotnet-parser.ts` — regex-based C# source analysis
|
|
3914
|
+
* Surface: `dotnet-surface.ts` — ApiSurface construction
|
|
3915
|
+
*/
|
|
3916
|
+
const DOTNET_PRIMITIVE_TYPES = new Set([
|
|
3917
|
+
"string",
|
|
3918
|
+
"int",
|
|
3919
|
+
"long",
|
|
3920
|
+
"float",
|
|
3921
|
+
"double",
|
|
3922
|
+
"bool",
|
|
3923
|
+
"decimal"
|
|
3924
|
+
]);
|
|
3925
|
+
const dotnetHints = {
|
|
3926
|
+
stripNullable(type) {
|
|
3927
|
+
const nullableMatch = type.match(/^Nullable<(.+)>$/);
|
|
3928
|
+
if (nullableMatch) return nullableMatch[1];
|
|
3929
|
+
if (type.endsWith("?")) return type.slice(0, -1);
|
|
3930
|
+
return null;
|
|
3931
|
+
},
|
|
3932
|
+
isNullableOnlyDifference(a, b) {
|
|
3933
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
3934
|
+
},
|
|
3935
|
+
isUnionReorder(_a, _b) {
|
|
3936
|
+
return false;
|
|
3937
|
+
},
|
|
3938
|
+
isGenericTypeParam(type) {
|
|
3939
|
+
return /^[A-Z]$/.test(type) || /^T[A-Z][a-zA-Z]*$/.test(type);
|
|
3940
|
+
},
|
|
3941
|
+
isExtractionArtifact(type) {
|
|
3942
|
+
return type === "object" || type === "dynamic";
|
|
3943
|
+
},
|
|
3944
|
+
tolerateCategoryMismatch: false,
|
|
3945
|
+
extractReturnTypeName(returnType) {
|
|
3946
|
+
let inner = returnType;
|
|
3947
|
+
const taskMatch = inner.match(/^Task<(.+)>$/);
|
|
3948
|
+
if (taskMatch) inner = taskMatch[1];
|
|
3949
|
+
if (inner === "Task") return null;
|
|
3950
|
+
if (inner.endsWith("?")) inner = inner.slice(0, -1);
|
|
3951
|
+
const listMatch = inner.match(/^(?:List|IList|IEnumerable|IReadOnlyList)<(.+)>$/);
|
|
3952
|
+
if (listMatch) inner = listMatch[1];
|
|
3953
|
+
if ([
|
|
3954
|
+
"string",
|
|
3955
|
+
"int",
|
|
3956
|
+
"long",
|
|
3957
|
+
"float",
|
|
3958
|
+
"double",
|
|
3959
|
+
"bool",
|
|
3960
|
+
"void",
|
|
3961
|
+
"decimal"
|
|
3962
|
+
].includes(inner)) return null;
|
|
3963
|
+
return inner;
|
|
3964
|
+
},
|
|
3965
|
+
extractParamTypeName(paramType) {
|
|
3966
|
+
let inner = paramType;
|
|
3967
|
+
if (inner.endsWith("?")) inner = inner.slice(0, -1);
|
|
3968
|
+
if ([
|
|
3969
|
+
"string",
|
|
3970
|
+
"int",
|
|
3971
|
+
"long",
|
|
3972
|
+
"float",
|
|
3973
|
+
"double",
|
|
3974
|
+
"bool",
|
|
3975
|
+
"decimal"
|
|
3976
|
+
].includes(inner)) return null;
|
|
3977
|
+
return inner;
|
|
3978
|
+
},
|
|
3979
|
+
propertyMatchesClass(propertyName, className) {
|
|
3980
|
+
return propertyName.toLowerCase() === className.toLowerCase();
|
|
3981
|
+
},
|
|
3982
|
+
derivedModelNames(modelName) {
|
|
3983
|
+
return [`${modelName}Response`];
|
|
3984
|
+
},
|
|
3985
|
+
isTypeEquivalent(baselineType, candidateType, candidateSurface) {
|
|
3986
|
+
const stripNullable = (t) => {
|
|
3987
|
+
if (t.endsWith("?")) return t.slice(0, -1);
|
|
3988
|
+
const m = t.match(/^Nullable<(.+)>$/);
|
|
3989
|
+
return m ? m[1] : t;
|
|
3990
|
+
};
|
|
3991
|
+
const baseClean = stripNullable(baselineType);
|
|
3992
|
+
const candClean = stripNullable(candidateType);
|
|
3993
|
+
const dictPattern = /^Dictionary<string,\s*(string|object|dynamic|Object)>$/;
|
|
3994
|
+
if (dictPattern.test(baseClean) && dictPattern.test(candClean)) return true;
|
|
3995
|
+
const arrayMatch = baseClean.match(/^(.+)\[\]$/);
|
|
3996
|
+
const listMatch = candClean.match(/^(?:List|IList|IEnumerable|IReadOnlyList)<(.+)>$/);
|
|
3997
|
+
if (arrayMatch && listMatch) {
|
|
3998
|
+
const baseInner = arrayMatch[1];
|
|
3999
|
+
const candInner = listMatch[1];
|
|
4000
|
+
if (baseInner === candInner) return true;
|
|
4001
|
+
if (NAMED_TYPE_RE.test(baseInner) && NAMED_TYPE_RE.test(candInner)) {
|
|
4002
|
+
if (typeExistsInSurface(candInner, candidateSurface)) {
|
|
4003
|
+
if (candInner.includes(baseInner) || baseInner.includes(candInner)) return true;
|
|
4004
|
+
const baseNoResp = baseInner.replace(/Response$/, "");
|
|
4005
|
+
const candNoResp = candInner.replace(/Response$/, "");
|
|
4006
|
+
if (candNoResp.includes(baseNoResp) || baseNoResp.includes(candNoResp)) return true;
|
|
4007
|
+
if (namedTypeWordsOverlap(baseInner, candInner)) return true;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
if (DOTNET_PRIMITIVE_TYPES.has(baseInner) && NAMED_TYPE_RE.test(candInner) || DOTNET_PRIMITIVE_TYPES.has(candInner) && NAMED_TYPE_RE.test(baseInner)) return true;
|
|
4011
|
+
}
|
|
4012
|
+
const baseListMatch = baseClean.match(/^(?:List|IList|IEnumerable|IReadOnlyList)<(.+)>$/);
|
|
4013
|
+
const candArrayMatch = candClean.match(/^(.+)\[\]$/);
|
|
4014
|
+
if (baseListMatch && candArrayMatch) {
|
|
4015
|
+
const baseInner = baseListMatch[1];
|
|
4016
|
+
const candInner = candArrayMatch[1];
|
|
4017
|
+
if (baseInner === candInner) return true;
|
|
4018
|
+
if (NAMED_TYPE_RE.test(baseInner) && NAMED_TYPE_RE.test(candInner)) {
|
|
4019
|
+
if (typeExistsInSurface(candInner, candidateSurface)) {
|
|
4020
|
+
if (candInner.includes(baseInner) || baseInner.includes(candInner)) return true;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
const genericPattern = /^(?:List|IList|IEnumerable|IReadOnlyList)<(.+)>$/;
|
|
4025
|
+
const baseGeneric = baseClean.match(genericPattern);
|
|
4026
|
+
const candGeneric = candClean.match(genericPattern);
|
|
4027
|
+
if (baseGeneric && candGeneric) {
|
|
4028
|
+
const baseInner = baseGeneric[1];
|
|
4029
|
+
const candInner = candGeneric[1];
|
|
4030
|
+
if (baseInner === candInner) return true;
|
|
4031
|
+
if (NAMED_TYPE_RE.test(baseInner) && NAMED_TYPE_RE.test(candInner)) {
|
|
4032
|
+
if (typeExistsInSurface(candInner, candidateSurface)) {
|
|
4033
|
+
if (candInner.includes(baseInner) || baseInner.includes(candInner)) return true;
|
|
4034
|
+
const baseNoResp = baseInner.replace(/Response$/, "");
|
|
4035
|
+
const candNoResp = candInner.replace(/Response$/, "");
|
|
4036
|
+
if (candNoResp.includes(baseNoResp) || baseNoResp.includes(candNoResp)) return true;
|
|
4037
|
+
if (namedTypeWordsOverlap(baseInner, candInner)) return true;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
if (NAMED_TYPE_RE.test(baseClean) && NAMED_TYPE_RE.test(candClean)) {
|
|
4042
|
+
if (typeExistsInSurface(candClean, candidateSurface)) {
|
|
4043
|
+
if (candClean.includes(baseClean) || baseClean.includes(candClean)) return true;
|
|
4044
|
+
const baseNoResp = baseClean.replace(/Response$/, "");
|
|
4045
|
+
const candNoResp = candClean.replace(/Response$/, "");
|
|
4046
|
+
if (candNoResp.includes(baseNoResp) || baseNoResp.includes(candNoResp)) return true;
|
|
4047
|
+
if (namedTypeWordsOverlap(baseClean, candClean)) return true;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
const numericTypes = new Set([
|
|
4051
|
+
"int",
|
|
4052
|
+
"long",
|
|
4053
|
+
"float",
|
|
4054
|
+
"double",
|
|
4055
|
+
"decimal"
|
|
4056
|
+
]);
|
|
4057
|
+
if (numericTypes.has(baseClean) && numericTypes.has(candClean)) return true;
|
|
4058
|
+
if (baseClean === "DateTime" && candClean === "string" || baseClean === "string" && candClean === "DateTime") return true;
|
|
4059
|
+
if (baseClean === "string" && candidateSurface.enums[candClean]) return true;
|
|
4060
|
+
if (candClean === "string" && candidateSurface.enums?.[baseClean]) return true;
|
|
4061
|
+
return false;
|
|
4062
|
+
},
|
|
4063
|
+
modelBaseClasses: [],
|
|
4064
|
+
exceptionBaseClasses: ["Exception"]
|
|
4065
|
+
};
|
|
4066
|
+
/** Create a DotNet extractor with optional hint overrides. */
|
|
4067
|
+
function createDotnetExtractor(hintOverrides) {
|
|
4068
|
+
const mergedHints = hintOverrides ? {
|
|
4069
|
+
...dotnetHints,
|
|
4070
|
+
...hintOverrides
|
|
4071
|
+
} : dotnetHints;
|
|
4072
|
+
return {
|
|
4073
|
+
language: "dotnet",
|
|
4074
|
+
hints: mergedHints,
|
|
4075
|
+
async extract(sdkPath) {
|
|
4076
|
+
let sourceDir;
|
|
4077
|
+
const srcPath = resolve(sdkPath, "src");
|
|
4078
|
+
try {
|
|
4079
|
+
statSync(srcPath);
|
|
4080
|
+
sourceDir = srcPath;
|
|
4081
|
+
} catch {
|
|
4082
|
+
sourceDir = sdkPath;
|
|
4083
|
+
}
|
|
4084
|
+
const csFiles = walkCSharpFiles(sourceDir);
|
|
4085
|
+
if (csFiles.length === 0) throw new ExtractorError(`No .cs files found in ${sdkPath}`, `Ensure the --sdk-path argument points to a .NET project root containing .cs source files.`);
|
|
4086
|
+
const allClasses = [];
|
|
4087
|
+
const allEnums = [];
|
|
4088
|
+
for (const filePath of csFiles) {
|
|
4089
|
+
const parsed = parseCSharpFile(filePath, sdkPath);
|
|
4090
|
+
allClasses.push(...parsed.classes);
|
|
4091
|
+
allEnums.push(...parsed.enums);
|
|
4092
|
+
}
|
|
4093
|
+
const { classes, interfaces, enums, exports } = buildSurface$1(allClasses, allEnums, mergedHints);
|
|
4094
|
+
return {
|
|
4095
|
+
language: "dotnet",
|
|
4096
|
+
extractedFrom: sdkPath,
|
|
4097
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4098
|
+
classes,
|
|
4099
|
+
interfaces,
|
|
4100
|
+
typeAliases: {},
|
|
4101
|
+
enums,
|
|
4102
|
+
exports
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
/** Default DotNet extractor with generic language hints. */
|
|
4108
|
+
const dotnetExtractor = createDotnetExtractor();
|
|
4109
|
+
//#endregion
|
|
4110
|
+
//#region src/compat/extractors/elixir-parser.ts
|
|
4111
|
+
/**
|
|
4112
|
+
* Elixir source parser — regex-based extraction of modules, structs,
|
|
4113
|
+
* functions, and type specs from Elixir source files.
|
|
4114
|
+
*/
|
|
4115
|
+
function walkElixirFiles(dir) {
|
|
4116
|
+
const results = [];
|
|
4117
|
+
let entries;
|
|
4118
|
+
try {
|
|
4119
|
+
entries = readdirSync(dir);
|
|
4120
|
+
} catch {
|
|
4121
|
+
return results;
|
|
4122
|
+
}
|
|
4123
|
+
for (const entry of entries) {
|
|
4124
|
+
const fullPath = join(dir, entry);
|
|
4125
|
+
let stat;
|
|
4126
|
+
try {
|
|
4127
|
+
stat = statSync(fullPath);
|
|
4128
|
+
} catch {
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
if (stat.isDirectory()) {
|
|
4132
|
+
if (entry.startsWith(".") || entry === "_build" || entry === "deps" || entry === "test") continue;
|
|
4133
|
+
results.push(...walkElixirFiles(fullPath));
|
|
4134
|
+
} else if (entry.endsWith(".ex") && !entry.endsWith("_test.exs")) results.push(fullPath);
|
|
4135
|
+
}
|
|
4136
|
+
return results.sort();
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Strip comments and heredoc strings from Elixir source code.
|
|
4140
|
+
* This prevents false matches of keywords like `do`/`end` inside strings
|
|
4141
|
+
* and doc comments.
|
|
4142
|
+
*/
|
|
4143
|
+
function stripCommentsAndHeredocs(source) {
|
|
4144
|
+
let result = source.replace(/"""[\s\S]*?"""/g, "\"\"\"\"\"\"");
|
|
4145
|
+
result = result.replace(/#[^\n]*/g, "");
|
|
4146
|
+
return result;
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Given the full source and the position right after `defmodule ... do`,
|
|
4150
|
+
* find the module body text up to the matching `end`.
|
|
4151
|
+
*
|
|
4152
|
+
* Uses a character-by-character scan that skips string literals and
|
|
4153
|
+
* counts block-level `do`/`end` keywords only when they appear as
|
|
4154
|
+
* standalone tokens (word boundaries).
|
|
4155
|
+
*/
|
|
4156
|
+
function extractModuleBody(source, startIdx) {
|
|
4157
|
+
let depth = 1;
|
|
4158
|
+
let i = startIdx;
|
|
4159
|
+
while (i < source.length && depth > 0) {
|
|
4160
|
+
const ch = source[i];
|
|
4161
|
+
if (ch === "\"") {
|
|
4162
|
+
i++;
|
|
4163
|
+
while (i < source.length && source[i] !== "\"") {
|
|
4164
|
+
if (source[i] === "\\") i++;
|
|
4165
|
+
i++;
|
|
4166
|
+
}
|
|
4167
|
+
i++;
|
|
4168
|
+
continue;
|
|
4169
|
+
}
|
|
4170
|
+
if (ch === "'") {
|
|
4171
|
+
i++;
|
|
4172
|
+
while (i < source.length && source[i] !== "'") {
|
|
4173
|
+
if (source[i] === "\\") i++;
|
|
4174
|
+
i++;
|
|
4175
|
+
}
|
|
4176
|
+
i++;
|
|
4177
|
+
continue;
|
|
4178
|
+
}
|
|
4179
|
+
if (isWordBoundary(source, i)) {
|
|
4180
|
+
if (source.slice(i, i + 2) === "do" && isWordEnd(source, i + 2)) {
|
|
4181
|
+
if (source[i + 2] !== ":") depth++;
|
|
4182
|
+
i += 2;
|
|
4183
|
+
continue;
|
|
4184
|
+
}
|
|
4185
|
+
if (source.slice(i, i + 2) === "fn" && isWordEnd(source, i + 2)) {
|
|
4186
|
+
depth++;
|
|
4187
|
+
i += 2;
|
|
4188
|
+
continue;
|
|
4189
|
+
}
|
|
4190
|
+
if (source.slice(i, i + 3) === "end" && isWordEnd(source, i + 3)) {
|
|
4191
|
+
depth--;
|
|
4192
|
+
if (depth === 0) return source.slice(startIdx, i);
|
|
4193
|
+
i += 3;
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
i++;
|
|
4198
|
+
}
|
|
4199
|
+
return null;
|
|
4200
|
+
}
|
|
4201
|
+
function isWordBoundary(source, idx) {
|
|
4202
|
+
if (idx === 0) return true;
|
|
4203
|
+
const prev = source[idx - 1];
|
|
4204
|
+
return !/[a-zA-Z0-9_]/.test(prev);
|
|
4205
|
+
}
|
|
4206
|
+
function isWordEnd(source, idx) {
|
|
4207
|
+
if (idx >= source.length) return true;
|
|
4208
|
+
return !/[a-zA-Z0-9_]/.test(source[idx]);
|
|
4209
|
+
}
|
|
4210
|
+
/** Find all defmodule blocks and extract their bodies in a single pass. */
|
|
4211
|
+
function extractModules(source) {
|
|
4212
|
+
const modules = [];
|
|
4213
|
+
const moduleRegex = /defmodule\s+([\w.]+)\s+do\b/g;
|
|
4214
|
+
let match;
|
|
4215
|
+
while ((match = moduleRegex.exec(source)) !== null) {
|
|
4216
|
+
const body = extractModuleBody(source, match.index + match[0].length);
|
|
4217
|
+
if (body) modules.push({
|
|
4218
|
+
name: match[1],
|
|
4219
|
+
body
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
return modules;
|
|
4223
|
+
}
|
|
4224
|
+
function extractStructs(modules, sourceFile) {
|
|
4225
|
+
const structs = [];
|
|
4226
|
+
for (const { name: moduleName, body: moduleBody } of modules) {
|
|
4227
|
+
const structMatch = moduleBody.match(/defstruct\s+\[([^\]]*)\]/);
|
|
4228
|
+
if (!structMatch) continue;
|
|
4229
|
+
const fieldsStr = structMatch[1];
|
|
4230
|
+
const fields = [];
|
|
4231
|
+
const fieldRegex = /:(\w+)/g;
|
|
4232
|
+
let fieldMatch;
|
|
4233
|
+
while ((fieldMatch = fieldRegex.exec(fieldsStr)) !== null) fields.push(fieldMatch[1]);
|
|
4234
|
+
if (fields.length > 0) structs.push({
|
|
4235
|
+
moduleName,
|
|
4236
|
+
fields,
|
|
4237
|
+
sourceFile
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
return structs;
|
|
4241
|
+
}
|
|
4242
|
+
function extractFunctions(modules, sourceFile) {
|
|
4243
|
+
const functions = [];
|
|
4244
|
+
for (const { name: moduleName, body: moduleBody } of modules) {
|
|
4245
|
+
const funcRegex = /(def|defp)\s+(\w+)(?:\(([^)]*)\))?(?:\s+do\b|\s*,\s*do:)/g;
|
|
4246
|
+
let funcMatch;
|
|
4247
|
+
while ((funcMatch = funcRegex.exec(moduleBody)) !== null) {
|
|
4248
|
+
const isPrivate = funcMatch[1] === "defp";
|
|
4249
|
+
const name = funcMatch[2];
|
|
4250
|
+
const paramsStr = funcMatch[3] || "";
|
|
4251
|
+
if (name === "defstruct" || name === "defmodule") continue;
|
|
4252
|
+
const params = [];
|
|
4253
|
+
if (paramsStr.trim()) {
|
|
4254
|
+
const paramParts = paramsStr.split(",");
|
|
4255
|
+
for (const part of paramParts) {
|
|
4256
|
+
const paramNameMatch = part.trim().match(/^(\w+)/);
|
|
4257
|
+
if (paramNameMatch) params.push(paramNameMatch[1]);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
functions.push({
|
|
4261
|
+
moduleName,
|
|
4262
|
+
name,
|
|
4263
|
+
arity: params.length,
|
|
4264
|
+
params,
|
|
4265
|
+
isPrivate,
|
|
4266
|
+
sourceFile
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
return functions;
|
|
4271
|
+
}
|
|
4272
|
+
function extractTypeSpecs(modules, sourceFile) {
|
|
4273
|
+
const specs = [];
|
|
4274
|
+
for (const { name: moduleName, body: moduleBody } of modules) {
|
|
4275
|
+
const typeRegex = /@type\s+(\w+)\s*::\s*/g;
|
|
4276
|
+
let typeMatch;
|
|
4277
|
+
while ((typeMatch = typeRegex.exec(moduleBody)) !== null) {
|
|
4278
|
+
const typeName = typeMatch[1];
|
|
4279
|
+
const defStart = typeMatch.index + typeMatch[0].length;
|
|
4280
|
+
let defEnd = defStart;
|
|
4281
|
+
let braceDepth = 0;
|
|
4282
|
+
for (let j = defStart; j < moduleBody.length; j++) {
|
|
4283
|
+
const ch = moduleBody[j];
|
|
4284
|
+
if (ch === "{") braceDepth++;
|
|
4285
|
+
else if (ch === "}") {
|
|
4286
|
+
braceDepth--;
|
|
4287
|
+
if (braceDepth < 0) break;
|
|
4288
|
+
if (braceDepth === 0) {
|
|
4289
|
+
defEnd = j + 1;
|
|
4290
|
+
break;
|
|
4291
|
+
}
|
|
4292
|
+
} else if (braceDepth === 0 && ch === "\n") {
|
|
4293
|
+
const remaining = moduleBody.slice(j + 1);
|
|
4294
|
+
if (remaining.match(/^\s*\S/)) {
|
|
4295
|
+
const nextContent = remaining.trimStart();
|
|
4296
|
+
if (nextContent.startsWith("@") || nextContent.startsWith("def ") || nextContent.startsWith("defp ") || nextContent.startsWith("defstruct") || nextContent.startsWith("defmodule")) {
|
|
4297
|
+
defEnd = j;
|
|
4298
|
+
break;
|
|
4299
|
+
}
|
|
4300
|
+
} else {
|
|
4301
|
+
defEnd = j;
|
|
4302
|
+
break;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
defEnd = j + 1;
|
|
4306
|
+
}
|
|
4307
|
+
const definition = moduleBody.slice(defStart, defEnd).trim();
|
|
4308
|
+
specs.push({
|
|
4309
|
+
moduleName,
|
|
4310
|
+
name: typeName,
|
|
4311
|
+
definition,
|
|
4312
|
+
sourceFile
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
return specs;
|
|
4317
|
+
}
|
|
4318
|
+
function extractEnumModules(modules, sourceFile) {
|
|
4319
|
+
const enumModules = [];
|
|
4320
|
+
for (const { name: moduleName, body: moduleBody } of modules) {
|
|
4321
|
+
if (!moduleBody.match(/def\s+values[\s,]/)) continue;
|
|
4322
|
+
if (moduleBody.match(/defstruct\b/)) continue;
|
|
4323
|
+
const members = {};
|
|
4324
|
+
const valueFuncRegex = /def\s+(\w+),\s*do:\s*"([^"]+)"/g;
|
|
4325
|
+
let valueFuncMatch;
|
|
4326
|
+
while ((valueFuncMatch = valueFuncRegex.exec(moduleBody)) !== null) {
|
|
4327
|
+
const funcName = valueFuncMatch[1];
|
|
4328
|
+
const funcValue = valueFuncMatch[2];
|
|
4329
|
+
if (funcName === "values") continue;
|
|
4330
|
+
members[funcName] = funcValue;
|
|
4331
|
+
}
|
|
4332
|
+
if (Object.keys(members).length > 0) enumModules.push({
|
|
4333
|
+
moduleName,
|
|
4334
|
+
members,
|
|
4335
|
+
sourceFile
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
return enumModules;
|
|
4339
|
+
}
|
|
4340
|
+
/** Parse a single Elixir source file and return all extracted symbols. */
|
|
4341
|
+
function parseElixirFile(filePath, sdkPath) {
|
|
4342
|
+
const source = readFileSync(filePath, "utf-8");
|
|
4343
|
+
const relPath = relative(sdkPath, filePath);
|
|
4344
|
+
const modules = extractModules(stripCommentsAndHeredocs(source));
|
|
4345
|
+
return {
|
|
4346
|
+
structs: extractStructs(modules, relPath),
|
|
4347
|
+
functions: extractFunctions(modules, relPath),
|
|
4348
|
+
typeSpecs: extractTypeSpecs(modules, relPath),
|
|
4349
|
+
enumModules: extractEnumModules(modules, relPath),
|
|
4350
|
+
moduleNames: modules.map((m) => m.name)
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
//#endregion
|
|
4354
|
+
//#region src/compat/extractors/elixir-surface.ts
|
|
4355
|
+
/** Get the short name from a fully qualified Elixir module name. */
|
|
4356
|
+
function shortName(moduleName) {
|
|
4357
|
+
const parts = moduleName.split(".");
|
|
4358
|
+
return parts[parts.length - 1];
|
|
4359
|
+
}
|
|
4360
|
+
function buildSurface(allStructs, allFunctions, allEnumModules, allTypeSpecs) {
|
|
4361
|
+
const classes = {};
|
|
4362
|
+
const interfaces = {};
|
|
4363
|
+
const enums = {};
|
|
4364
|
+
const collector = new ExportCollector();
|
|
4365
|
+
const structModuleNames = new Set(allStructs.map((s) => s.moduleName));
|
|
4366
|
+
const enumModuleNames = new Set(allEnumModules.map((e) => e.moduleName));
|
|
4367
|
+
for (const struct of allStructs) {
|
|
4368
|
+
const name = shortName(struct.moduleName);
|
|
4369
|
+
const fields = {};
|
|
4370
|
+
const typeSpec = allTypeSpecs.find((ts) => ts.moduleName === struct.moduleName && ts.name === "t");
|
|
4371
|
+
for (const field of struct.fields) {
|
|
4372
|
+
let fieldType = "any";
|
|
4373
|
+
let optional = false;
|
|
4374
|
+
if (typeSpec) {
|
|
4375
|
+
const fieldPrefix = field + ":";
|
|
4376
|
+
const prefixIdx = typeSpec.definition.indexOf(fieldPrefix);
|
|
4377
|
+
const fieldTypeMatch = prefixIdx >= 0 ? typeSpec.definition.slice(prefixIdx + fieldPrefix.length).match(/^\s*([^,}]+)/) : null;
|
|
4378
|
+
if (fieldTypeMatch) {
|
|
4379
|
+
fieldType = fieldTypeMatch[1].trim();
|
|
4380
|
+
if (fieldType.includes("| nil") || fieldType.includes("nil |")) {
|
|
4381
|
+
optional = true;
|
|
4382
|
+
fieldType = fieldType.replace(/\s*\|\s*nil/g, "").replace(/nil\s*\|\s*/g, "").trim();
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
fields[field] = {
|
|
4387
|
+
name: field,
|
|
4388
|
+
type: fieldType,
|
|
4389
|
+
optional
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4392
|
+
interfaces[name] = {
|
|
4393
|
+
name,
|
|
4394
|
+
sourceFile: struct.sourceFile,
|
|
4395
|
+
fields: sortRecord(fields),
|
|
4396
|
+
extends: []
|
|
4397
|
+
};
|
|
4398
|
+
collector.add(struct.sourceFile, name);
|
|
4399
|
+
}
|
|
4400
|
+
for (const enumModule of allEnumModules) {
|
|
4401
|
+
const name = shortName(enumModule.moduleName);
|
|
4402
|
+
enums[name] = {
|
|
4403
|
+
name,
|
|
4404
|
+
sourceFile: enumModule.sourceFile,
|
|
4405
|
+
members: sortRecord(enumModule.members)
|
|
4406
|
+
};
|
|
4407
|
+
collector.add(enumModule.sourceFile, name);
|
|
4408
|
+
}
|
|
4409
|
+
const funcsByModule = /* @__PURE__ */ new Map();
|
|
4410
|
+
for (const func of allFunctions) {
|
|
4411
|
+
if (func.isPrivate) continue;
|
|
4412
|
+
if (structModuleNames.has(func.moduleName) && !funcsByModule.has(func.moduleName)) {}
|
|
4413
|
+
if (enumModuleNames.has(func.moduleName)) continue;
|
|
4414
|
+
if (!funcsByModule.has(func.moduleName)) funcsByModule.set(func.moduleName, []);
|
|
4415
|
+
funcsByModule.get(func.moduleName).push(func);
|
|
4416
|
+
}
|
|
4417
|
+
for (const [moduleName, funcs] of funcsByModule) {
|
|
4418
|
+
if (structModuleNames.has(moduleName)) continue;
|
|
4419
|
+
const name = shortName(moduleName);
|
|
4420
|
+
const methods = {};
|
|
4421
|
+
const seenFuncs = /* @__PURE__ */ new Set();
|
|
4422
|
+
for (const func of funcs) {
|
|
4423
|
+
if (seenFuncs.has(func.name)) continue;
|
|
4424
|
+
seenFuncs.add(func.name);
|
|
4425
|
+
const params = func.params.map((p) => ({
|
|
4426
|
+
name: p,
|
|
4427
|
+
type: "any",
|
|
4428
|
+
optional: false
|
|
4429
|
+
}));
|
|
4430
|
+
if (!methods[func.name]) methods[func.name] = [];
|
|
4431
|
+
methods[func.name].push({
|
|
4432
|
+
name: func.name,
|
|
4433
|
+
params,
|
|
4434
|
+
returnType: "any",
|
|
4435
|
+
async: false
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
if (Object.keys(methods).length > 0) {
|
|
4439
|
+
classes[name] = {
|
|
4440
|
+
name,
|
|
4441
|
+
sourceFile: funcs[0].sourceFile,
|
|
4442
|
+
methods: sortRecord(methods),
|
|
4443
|
+
properties: {},
|
|
4444
|
+
constructorParams: []
|
|
4445
|
+
};
|
|
4446
|
+
collector.add(funcs[0].sourceFile, name);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
const exports = collector.toRecord();
|
|
4450
|
+
return {
|
|
4451
|
+
classes: sortRecord(classes),
|
|
4452
|
+
interfaces: sortRecord(interfaces),
|
|
4453
|
+
enums: sortRecord(enums),
|
|
4454
|
+
exports: sortRecord(exports)
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
const elixirExtractor = {
|
|
4458
|
+
language: "elixir",
|
|
4459
|
+
hints: {
|
|
4460
|
+
stripNullable(type) {
|
|
4461
|
+
if (type.includes("| nil")) return type.replace(/\s*\|\s*nil/g, "").trim();
|
|
4462
|
+
if (type.includes("nil |")) return type.replace(/nil\s*\|\s*/g, "").trim();
|
|
4463
|
+
return null;
|
|
4464
|
+
},
|
|
4465
|
+
isNullableOnlyDifference(a, b) {
|
|
4466
|
+
return defaultIsNullableOnlyDifference(this, a, b);
|
|
4467
|
+
},
|
|
4468
|
+
isUnionReorder(a, b) {
|
|
4469
|
+
const sortParts = (t) => t.split("|").map((s) => s.trim()).sort().join("|");
|
|
4470
|
+
return a !== b && sortParts(a) === sortParts(b);
|
|
4471
|
+
},
|
|
4472
|
+
isGenericTypeParam(type) {
|
|
4473
|
+
return type === "any" || type === "term";
|
|
4474
|
+
},
|
|
4475
|
+
isExtractionArtifact(type) {
|
|
4476
|
+
return type === "any" || type === "term" || type === "any()";
|
|
4477
|
+
},
|
|
4478
|
+
tolerateCategoryMismatch: true,
|
|
4479
|
+
extractReturnTypeName(returnType) {
|
|
4480
|
+
let inner = returnType;
|
|
4481
|
+
const okMatch = inner.match(/^\{:ok,\s*(.+)\}$/);
|
|
4482
|
+
if (okMatch) inner = okMatch[1].trim();
|
|
4483
|
+
const listMatch = inner.match(/^\[(.+)\]$/);
|
|
4484
|
+
if (listMatch) inner = listMatch[1].trim();
|
|
4485
|
+
if ([
|
|
4486
|
+
"any",
|
|
4487
|
+
"term",
|
|
4488
|
+
"atom",
|
|
4489
|
+
"binary",
|
|
4490
|
+
"boolean",
|
|
4491
|
+
"integer",
|
|
4492
|
+
"float",
|
|
4493
|
+
"number",
|
|
4494
|
+
"pid"
|
|
4495
|
+
].includes(inner)) return null;
|
|
4496
|
+
const typeCallMatch = inner.match(/^(\w+)\.t\(\)$/);
|
|
4497
|
+
if (typeCallMatch) return typeCallMatch[1];
|
|
4498
|
+
return inner || null;
|
|
4499
|
+
},
|
|
4500
|
+
extractParamTypeName(paramType) {
|
|
4501
|
+
if ([
|
|
4502
|
+
"any",
|
|
4503
|
+
"term",
|
|
4504
|
+
"atom",
|
|
4505
|
+
"binary",
|
|
4506
|
+
"boolean",
|
|
4507
|
+
"integer",
|
|
4508
|
+
"float",
|
|
4509
|
+
"number",
|
|
4510
|
+
"pid",
|
|
4511
|
+
"keyword"
|
|
4512
|
+
].includes(paramType)) return null;
|
|
4513
|
+
const typeCallMatch = paramType.match(/^(\w+)\.t\(\)$/);
|
|
4514
|
+
if (typeCallMatch) return typeCallMatch[1];
|
|
4515
|
+
return paramType;
|
|
4516
|
+
},
|
|
4517
|
+
propertyMatchesClass(propertyName, className) {
|
|
4518
|
+
return propertyName.replace(/_/g, "").toLowerCase() === className.toLowerCase();
|
|
4519
|
+
},
|
|
4520
|
+
derivedModelNames(modelName) {
|
|
4521
|
+
return [`${modelName}Response`];
|
|
4522
|
+
}
|
|
4523
|
+
},
|
|
4524
|
+
async extract(sdkPath) {
|
|
4525
|
+
let sourceDir;
|
|
4526
|
+
const libPath = resolve(sdkPath, "lib");
|
|
4527
|
+
try {
|
|
4528
|
+
statSync(libPath);
|
|
4529
|
+
sourceDir = libPath;
|
|
4530
|
+
} catch {
|
|
4531
|
+
sourceDir = sdkPath;
|
|
4532
|
+
}
|
|
4533
|
+
const exFiles = walkElixirFiles(sourceDir);
|
|
4534
|
+
if (exFiles.length === 0) throw new ExtractorError(`No .ex files found in ${sdkPath}`, `Ensure the --sdk-path argument points to an Elixir project root containing .ex source files in lib/.`);
|
|
4535
|
+
const allStructs = [];
|
|
4536
|
+
const allFunctions = [];
|
|
4537
|
+
const allEnumModules = [];
|
|
4538
|
+
const allTypeSpecs = [];
|
|
4539
|
+
for (const filePath of exFiles) {
|
|
4540
|
+
const parsed = parseElixirFile(filePath, sdkPath);
|
|
4541
|
+
allStructs.push(...parsed.structs);
|
|
4542
|
+
allFunctions.push(...parsed.functions);
|
|
4543
|
+
allEnumModules.push(...parsed.enumModules);
|
|
4544
|
+
allTypeSpecs.push(...parsed.typeSpecs);
|
|
4545
|
+
}
|
|
4546
|
+
const { classes, interfaces, enums, exports } = buildSurface(allStructs, allFunctions, allEnumModules, allTypeSpecs);
|
|
4547
|
+
return {
|
|
4548
|
+
language: "elixir",
|
|
4549
|
+
extractedFrom: sdkPath,
|
|
4550
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4551
|
+
classes,
|
|
4552
|
+
interfaces,
|
|
4553
|
+
typeAliases: {},
|
|
4554
|
+
enums,
|
|
4555
|
+
exports
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
};
|
|
4559
|
+
//#endregion
|
|
4560
|
+
export { buildOverlayLookup, diffSurfaces, dotnetExtractor, elixirExtractor, filterSurface, getExtractor, goExtractor, kotlinExtractor, nodeExtractor, nodeHints, patchOverlay, phpExtractor, pythonExtractor, registerExtractor, resolveHints, rubyExtractor, rustExtractor, specDerivedEnumValues, specDerivedFieldPaths, specDerivedHttpKeys, specDerivedMethodPaths, specDerivedNames };
|
|
4561
|
+
|
|
4562
|
+
//# sourceMappingURL=index.mjs.map
|