@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,2432 @@
|
|
|
1
|
+
import { A as walkTypeRef, C as assertNever, f as toSnakeCase, i as normalizeJsExtension, t as safeParse, x as RegistryError } from "./tree-sitter-DFRXfWg9.mjs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import Parser from "tree-sitter";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
//#region src/engine/merge-adapters/url-fingerprint.ts
|
|
9
|
+
/**
|
|
10
|
+
* Extract a URL path fingerprint from a method body using tree-sitter AST traversal.
|
|
11
|
+
*
|
|
12
|
+
* Walks the AST subtree of the member node looking for the first string literal
|
|
13
|
+
* that starts with '/'. Normalizes interpolation expressions to '{}' and format
|
|
14
|
+
* specifiers (%s, %d, etc.) to '{}' for cross-language comparison.
|
|
15
|
+
*
|
|
16
|
+
* @returns A normalized URL fingerprint like "/organizations/{}" or undefined if none found.
|
|
17
|
+
*/
|
|
18
|
+
function extractUrlFingerprint(memberNode, config) {
|
|
19
|
+
const stringTypes = new Set(config.stringNodeTypes);
|
|
20
|
+
const contentTypes = new Set(config.contentNodeTypes);
|
|
21
|
+
const interpolationTypes = new Set(config.interpolationNodeTypes);
|
|
22
|
+
const formatFns = config.formatFunctionNames ? new Set(config.formatFunctionNames) : null;
|
|
23
|
+
const stack = [memberNode];
|
|
24
|
+
while (stack.length > 0) {
|
|
25
|
+
const node = stack.pop();
|
|
26
|
+
if (stringTypes.has(node.type)) {
|
|
27
|
+
const fp = resolveFingerprint(node, contentTypes, interpolationTypes);
|
|
28
|
+
if (fp?.startsWith("/")) return fp;
|
|
29
|
+
}
|
|
30
|
+
if (formatFns && isFormatCall(node, formatFns)) {
|
|
31
|
+
const fp = extractFromFormatCall(node, stringTypes, contentTypes, interpolationTypes);
|
|
32
|
+
if (fp?.startsWith("/")) return fp;
|
|
33
|
+
}
|
|
34
|
+
for (let i = node.childCount - 1; i >= 0; i--) {
|
|
35
|
+
const child = node.child(i);
|
|
36
|
+
if (child) stack.push(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the text content of a string node, normalizing interpolations to '{}'.
|
|
42
|
+
*/
|
|
43
|
+
function resolveFingerprint(stringNode, contentTypes, interpolationTypes) {
|
|
44
|
+
if (stringNode.namedChildCount === 0) return normalizeFormatSpecifiers(stripQuotes(stringNode.text));
|
|
45
|
+
let result = "";
|
|
46
|
+
for (let i = 0; i < stringNode.childCount; i++) {
|
|
47
|
+
const child = stringNode.child(i);
|
|
48
|
+
if (contentTypes.has(child.type)) result += child.text;
|
|
49
|
+
else if (interpolationTypes.has(child.type)) result += "{}";
|
|
50
|
+
}
|
|
51
|
+
return result ? normalizeFormatSpecifiers(result) : normalizeFormatSpecifiers(stripQuotes(stringNode.text));
|
|
52
|
+
}
|
|
53
|
+
/** Check if a node is a call to one of the format functions (e.g., fmt.Sprintf, format!). */
|
|
54
|
+
function isFormatCall(node, formatFns) {
|
|
55
|
+
if (node.type === "macro_invocation") {
|
|
56
|
+
const macroName = node.childForFieldName("macro")?.text;
|
|
57
|
+
return !!(macroName && formatFns.has(macroName.replace(/!$/, "")));
|
|
58
|
+
}
|
|
59
|
+
if (node.type !== "call_expression") return false;
|
|
60
|
+
const fnNode = node.childForFieldName("function");
|
|
61
|
+
if (!fnNode) return false;
|
|
62
|
+
if (formatFns.has(fnNode.text)) return true;
|
|
63
|
+
if (fnNode.type === "selector_expression" || fnNode.type === "member_expression") {
|
|
64
|
+
const prop = fnNode.childForFieldName("field") ?? fnNode.childForFieldName("property");
|
|
65
|
+
if (prop && formatFns.has(prop.text)) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/** Extract the first string argument from a format function call. */
|
|
70
|
+
function extractFromFormatCall(callNode, stringTypes, contentTypes, interpolationTypes) {
|
|
71
|
+
const args = callNode.childForFieldName("arguments");
|
|
72
|
+
if (!args) return void 0;
|
|
73
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
74
|
+
const arg = args.child(i);
|
|
75
|
+
if (stringTypes.has(arg.type)) return resolveFingerprint(arg, contentTypes, interpolationTypes);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Strip surrounding quote characters from a string literal. */
|
|
79
|
+
function stripQuotes(text) {
|
|
80
|
+
const match = text.match(/^(?:f|r|b|@|\$)?(?:"""([\s\S]*)"""|'''([\s\S]*)'''|"(.*)"|'(.*)'|`(.*)`)$/s);
|
|
81
|
+
if (match) return match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? text;
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
/** Replace printf-style format specifiers (%s, %d, %v, %f) with {}. */
|
|
85
|
+
function normalizeFormatSpecifiers(s) {
|
|
86
|
+
return s.replace(/%[sdvf]/g, "{}");
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/engine/merge-adapters/dotnet.ts
|
|
90
|
+
const DOTNET_URL_FINGERPRINT_CONFIG = {
|
|
91
|
+
stringNodeTypes: ["string_literal", "interpolated_string_expression"],
|
|
92
|
+
contentNodeTypes: ["string_content", "string_literal_content"],
|
|
93
|
+
interpolationNodeTypes: ["interpolation"]
|
|
94
|
+
};
|
|
95
|
+
const DECLARATION_TYPES = new Set([
|
|
96
|
+
"class_declaration",
|
|
97
|
+
"struct_declaration",
|
|
98
|
+
"interface_declaration",
|
|
99
|
+
"enum_declaration",
|
|
100
|
+
"record_declaration"
|
|
101
|
+
]);
|
|
102
|
+
function extractDeclarationName$1(node) {
|
|
103
|
+
if (DECLARATION_TYPES.has(node.type)) return (node.childForFieldName("name") ?? node.children.find((c) => c.type === "identifier"))?.text ?? null;
|
|
104
|
+
if (node.type === "namespace_declaration" || node.type === "file_scoped_namespace_declaration") {
|
|
105
|
+
const nameNode = node.children.find((c) => c.type === "qualified_name") ?? node.children.find((c) => c.type === "identifier");
|
|
106
|
+
return nameNode ? `__namespace:${nameNode.text}` : null;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function collectPrecedingXmlDocComments(children, index, source) {
|
|
111
|
+
let lastIdx = -1;
|
|
112
|
+
let firstIdx = -1;
|
|
113
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
114
|
+
const prev = children[k];
|
|
115
|
+
if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
|
|
116
|
+
if (lastIdx === -1) lastIdx = k;
|
|
117
|
+
firstIdx = k;
|
|
118
|
+
} else break;
|
|
119
|
+
else break;
|
|
120
|
+
}
|
|
121
|
+
if (firstIdx === -1) return null;
|
|
122
|
+
const first = children[firstIdx];
|
|
123
|
+
const last = children[lastIdx];
|
|
124
|
+
return {
|
|
125
|
+
text: source.slice(first.startIndex, last.endIndex),
|
|
126
|
+
startIndex: first.startIndex,
|
|
127
|
+
endIndex: last.endIndex
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Collect declarations from a list of sibling nodes (root level or namespace body).
|
|
132
|
+
*/
|
|
133
|
+
function collectDeclarations(children, source, statements, imports, importAnchors, importInsertionAnchor) {
|
|
134
|
+
for (const child of children) {
|
|
135
|
+
if (child.type === "comment") continue;
|
|
136
|
+
if (child.type === "using_directive") {
|
|
137
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
138
|
+
imports.push({
|
|
139
|
+
key: text.trim(),
|
|
140
|
+
text
|
|
141
|
+
});
|
|
142
|
+
importAnchors.push(text);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (child.type === "file_scoped_namespace_declaration") {
|
|
146
|
+
importInsertionAnchor.value = source.slice(child.startIndex, child.endIndex);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (child.type === "namespace_declaration") {
|
|
150
|
+
importInsertionAnchor.value = source.slice(child.startIndex, child.endIndex);
|
|
151
|
+
const body = child.children.find((c) => c.type === "declaration_list");
|
|
152
|
+
if (body) collectDeclarations(body.namedChildren, source, statements, imports, importAnchors, importInsertionAnchor);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const name = extractDeclarationName$1(child);
|
|
156
|
+
statements.push({
|
|
157
|
+
key: name,
|
|
158
|
+
kind: name ? "declaration" : "other",
|
|
159
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const dotnetMergeAdapter = {
|
|
164
|
+
language: "dotnet",
|
|
165
|
+
grammarModule: "tree-sitter-c-sharp",
|
|
166
|
+
testFilePatterns: [/Tests?\.cs$/],
|
|
167
|
+
urlFingerprintConfig: DOTNET_URL_FINGERPRINT_CONFIG,
|
|
168
|
+
parseStatements(tree, source) {
|
|
169
|
+
const imports = [];
|
|
170
|
+
const importAnchors = [];
|
|
171
|
+
const statements = [];
|
|
172
|
+
const importInsertionAnchor = {};
|
|
173
|
+
collectDeclarations(tree.rootNode.children, source, statements, imports, importAnchors, importInsertionAnchor);
|
|
174
|
+
return {
|
|
175
|
+
imports,
|
|
176
|
+
importAnchors,
|
|
177
|
+
importInsertionAnchor: importInsertionAnchor.value,
|
|
178
|
+
statements
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
renderImports(imports) {
|
|
182
|
+
return imports.map((entry) => entry.text);
|
|
183
|
+
},
|
|
184
|
+
extractDocstrings(tree, source) {
|
|
185
|
+
const result = /* @__PURE__ */ new Map();
|
|
186
|
+
function processChildren(children) {
|
|
187
|
+
for (let i = 0; i < children.length; i++) {
|
|
188
|
+
const child = children[i];
|
|
189
|
+
if (child.type === "namespace_declaration") {
|
|
190
|
+
const body = child.children.find((c) => c.type === "declaration_list");
|
|
191
|
+
if (body) processChildren(body.children);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const name = extractDeclarationName$1(child);
|
|
195
|
+
if (!name || name.startsWith("__namespace:")) continue;
|
|
196
|
+
const docstring = collectPrecedingXmlDocComments(children, i, source);
|
|
197
|
+
const members = /* @__PURE__ */ new Map();
|
|
198
|
+
const body = child.children.find((c) => c.type === "declaration_list");
|
|
199
|
+
if (body) {
|
|
200
|
+
const bodyChildren = body.children;
|
|
201
|
+
for (let j = 0; j < bodyChildren.length; j++) {
|
|
202
|
+
const member = bodyChildren[j];
|
|
203
|
+
if (member.type !== "method_declaration" && member.type !== "property_declaration") continue;
|
|
204
|
+
const identifiers = member.children.filter((c) => c.type === "identifier");
|
|
205
|
+
const memberName = member.type === "method_declaration" ? identifiers.length >= 2 ? identifiers[1].text : identifiers[0]?.text : identifiers[0]?.text;
|
|
206
|
+
if (!memberName) continue;
|
|
207
|
+
const memberDoc = collectPrecedingXmlDocComments(bodyChildren, j, source);
|
|
208
|
+
members.set(memberName, {
|
|
209
|
+
docstring: memberDoc,
|
|
210
|
+
declStartIndex: member.startIndex,
|
|
211
|
+
declColumn: member.startPosition.column,
|
|
212
|
+
urlFingerprint: extractUrlFingerprint(member, DOTNET_URL_FINGERPRINT_CONFIG)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
result.set(name, {
|
|
217
|
+
docstring,
|
|
218
|
+
declStartIndex: child.startIndex,
|
|
219
|
+
declColumn: child.startPosition.column,
|
|
220
|
+
members
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
processChildren(tree.rootNode.children);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/engine/merge-adapters/elixir.ts
|
|
230
|
+
const ELIXIR_URL_FINGERPRINT_CONFIG = {
|
|
231
|
+
stringNodeTypes: ["string"],
|
|
232
|
+
contentNodeTypes: ["quoted_content"],
|
|
233
|
+
interpolationNodeTypes: ["interpolation"]
|
|
234
|
+
};
|
|
235
|
+
function isDefmodule(node) {
|
|
236
|
+
return node.type === "call" && node.children[0]?.text === "defmodule";
|
|
237
|
+
}
|
|
238
|
+
function getModuleName(node) {
|
|
239
|
+
if (!isDefmodule(node)) return null;
|
|
240
|
+
const args = node.children.find((c) => c.type === "arguments");
|
|
241
|
+
if (!args) return null;
|
|
242
|
+
return args.children.find((c) => c.type === "alias")?.text ?? null;
|
|
243
|
+
}
|
|
244
|
+
function getDoBlock(node) {
|
|
245
|
+
return node.children.find((c) => c.type === "do_block") ?? null;
|
|
246
|
+
}
|
|
247
|
+
/** Check if a node is an @moduledoc or @doc attribute. */
|
|
248
|
+
function isDocAttribute(node) {
|
|
249
|
+
if (node.type !== "unary_operator") return null;
|
|
250
|
+
const call = node.children.find((c) => c.type === "call");
|
|
251
|
+
if (!call) return null;
|
|
252
|
+
const id = call.children.find((c) => c.type === "identifier");
|
|
253
|
+
if (id?.text === "moduledoc" || id?.text === "doc") return id.text;
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
/** Check if a node is a def/defp call. */
|
|
257
|
+
function getDefName(node) {
|
|
258
|
+
if (node.type !== "call") return null;
|
|
259
|
+
const id = node.children[0];
|
|
260
|
+
if (!id || id.text !== "def" && id.text !== "defp") return null;
|
|
261
|
+
const args = node.children.find((c) => c.type === "arguments");
|
|
262
|
+
if (!args) return null;
|
|
263
|
+
for (const arg of args.namedChildren) {
|
|
264
|
+
if (arg.type === "identifier") return arg.text;
|
|
265
|
+
if (arg.type === "call") return arg.children.find((c) => c.type === "identifier")?.text ?? null;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
/** Check if a node is an alias/import/require/use call. */
|
|
270
|
+
function isImportLike(node) {
|
|
271
|
+
if (node.type !== "call") return false;
|
|
272
|
+
const id = node.children[0];
|
|
273
|
+
return id?.type === "identifier" && [
|
|
274
|
+
"alias",
|
|
275
|
+
"import",
|
|
276
|
+
"require",
|
|
277
|
+
"use"
|
|
278
|
+
].includes(id.text);
|
|
279
|
+
}
|
|
280
|
+
const elixirMergeAdapter = {
|
|
281
|
+
language: "elixir",
|
|
282
|
+
grammarModule: "tree-sitter-elixir",
|
|
283
|
+
testFilePatterns: [/_test\.exs$/],
|
|
284
|
+
urlFingerprintConfig: ELIXIR_URL_FINGERPRINT_CONFIG,
|
|
285
|
+
parseStatements(tree, source) {
|
|
286
|
+
const imports = [];
|
|
287
|
+
const importAnchors = [];
|
|
288
|
+
const statements = [];
|
|
289
|
+
for (const child of tree.rootNode.children) {
|
|
290
|
+
if (child.type === "comment") continue;
|
|
291
|
+
if (isImportLike(child)) {
|
|
292
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
293
|
+
imports.push({
|
|
294
|
+
key: text.trim(),
|
|
295
|
+
text
|
|
296
|
+
});
|
|
297
|
+
importAnchors.push(text);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const moduleName = getModuleName(child);
|
|
301
|
+
statements.push({
|
|
302
|
+
key: moduleName,
|
|
303
|
+
kind: moduleName ? "declaration" : "other",
|
|
304
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
imports,
|
|
309
|
+
importAnchors,
|
|
310
|
+
statements
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
renderImports(imports) {
|
|
314
|
+
return imports.map((entry) => entry.text);
|
|
315
|
+
},
|
|
316
|
+
extractDocstrings(tree, source) {
|
|
317
|
+
const result = /* @__PURE__ */ new Map();
|
|
318
|
+
const rootChildren = tree.rootNode.children;
|
|
319
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
320
|
+
const child = rootChildren[i];
|
|
321
|
+
const moduleName = getModuleName(child);
|
|
322
|
+
if (!moduleName) continue;
|
|
323
|
+
let docstring = null;
|
|
324
|
+
const doBlock = getDoBlock(child);
|
|
325
|
+
const members = /* @__PURE__ */ new Map();
|
|
326
|
+
if (doBlock) {
|
|
327
|
+
const bodyChildren = doBlock.children;
|
|
328
|
+
for (const bodyChild of bodyChildren) if (isDocAttribute(bodyChild) === "moduledoc") {
|
|
329
|
+
docstring = {
|
|
330
|
+
text: source.slice(bodyChild.startIndex, bodyChild.endIndex),
|
|
331
|
+
startIndex: bodyChild.startIndex,
|
|
332
|
+
endIndex: bodyChild.endIndex
|
|
333
|
+
};
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
for (let j = 0; j < bodyChildren.length; j++) {
|
|
337
|
+
const bodyChild = bodyChildren[j];
|
|
338
|
+
const defName = getDefName(bodyChild);
|
|
339
|
+
if (!defName) continue;
|
|
340
|
+
let memberDoc = null;
|
|
341
|
+
for (let k = j - 1; k >= 0; k--) {
|
|
342
|
+
const prev = bodyChildren[k];
|
|
343
|
+
if (isDocAttribute(prev) === "doc") {
|
|
344
|
+
memberDoc = {
|
|
345
|
+
text: source.slice(prev.startIndex, prev.endIndex),
|
|
346
|
+
startIndex: prev.startIndex,
|
|
347
|
+
endIndex: prev.endIndex
|
|
348
|
+
};
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (prev.type === "comment") continue;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
members.set(defName, {
|
|
355
|
+
docstring: memberDoc,
|
|
356
|
+
declStartIndex: bodyChild.startIndex,
|
|
357
|
+
declColumn: bodyChild.startPosition.column,
|
|
358
|
+
urlFingerprint: extractUrlFingerprint(bodyChild, ELIXIR_URL_FINGERPRINT_CONFIG)
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
result.set(moduleName, {
|
|
363
|
+
docstring,
|
|
364
|
+
declStartIndex: child.startIndex,
|
|
365
|
+
declColumn: child.startPosition.column,
|
|
366
|
+
members
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/engine/merge-adapters/go.ts
|
|
374
|
+
const GO_URL_FINGERPRINT_CONFIG = {
|
|
375
|
+
stringNodeTypes: ["interpreted_string_literal"],
|
|
376
|
+
contentNodeTypes: ["interpreted_string_literal_content"],
|
|
377
|
+
interpolationNodeTypes: [],
|
|
378
|
+
formatFunctionNames: ["Sprintf"]
|
|
379
|
+
};
|
|
380
|
+
function lastIdentifier(text) {
|
|
381
|
+
const matches = [...text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)];
|
|
382
|
+
return matches.length > 0 ? matches[matches.length - 1][0] : null;
|
|
383
|
+
}
|
|
384
|
+
function receiverKey(node) {
|
|
385
|
+
const receiver = node.childForFieldName("receiver");
|
|
386
|
+
if (!receiver) return null;
|
|
387
|
+
return lastIdentifier(receiver.text);
|
|
388
|
+
}
|
|
389
|
+
function declarationKey$1(node) {
|
|
390
|
+
switch (node.type) {
|
|
391
|
+
case "package_clause": {
|
|
392
|
+
const pkg = lastIdentifier(node.text);
|
|
393
|
+
return pkg ? `__package:${pkg}` : "__package:";
|
|
394
|
+
}
|
|
395
|
+
case "type_declaration": return ((node.firstNamedChild?.type === "type_spec" ? node.firstNamedChild : node.namedChildren[0])?.childForFieldName("name"))?.text ?? null;
|
|
396
|
+
case "function_declaration": return node.childForFieldName("name")?.text ?? null;
|
|
397
|
+
case "method_declaration": {
|
|
398
|
+
const name = node.childForFieldName("name")?.text ?? null;
|
|
399
|
+
const receiver = receiverKey(node);
|
|
400
|
+
return name && receiver ? `method:${receiver}.${name}` : name;
|
|
401
|
+
}
|
|
402
|
+
case "const_declaration":
|
|
403
|
+
case "var_declaration": return node.text.split(/\s+/).slice(0, 3).join(" ");
|
|
404
|
+
default: return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function importEntries(node, source) {
|
|
408
|
+
const entries = [];
|
|
409
|
+
const collect = (spec) => {
|
|
410
|
+
const path = spec.childForFieldName("path")?.text ?? spec.text;
|
|
411
|
+
const name = spec.childForFieldName("name")?.text ?? "";
|
|
412
|
+
const text = source.slice(spec.startIndex, spec.endIndex);
|
|
413
|
+
entries.push({
|
|
414
|
+
key: `${name}:${path.replace(/^"|"$/g, "")}`,
|
|
415
|
+
text
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
for (const child of node.namedChildren) {
|
|
419
|
+
if (child.type === "import_spec") {
|
|
420
|
+
collect(child);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (child.type === "import_spec_list") {
|
|
424
|
+
for (const spec of child.namedChildren) if (spec.type === "import_spec") collect(spec);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return entries;
|
|
428
|
+
}
|
|
429
|
+
function collectPrecedingGoComments(children, index, source) {
|
|
430
|
+
let lastIdx = -1;
|
|
431
|
+
let firstIdx = -1;
|
|
432
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
433
|
+
const prev = children[k];
|
|
434
|
+
if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("//")) {
|
|
435
|
+
if (lastIdx === -1) lastIdx = k;
|
|
436
|
+
firstIdx = k;
|
|
437
|
+
} else break;
|
|
438
|
+
else break;
|
|
439
|
+
}
|
|
440
|
+
if (firstIdx === -1) return null;
|
|
441
|
+
const first = children[firstIdx];
|
|
442
|
+
const last = children[lastIdx];
|
|
443
|
+
return {
|
|
444
|
+
text: source.slice(first.startIndex, last.endIndex),
|
|
445
|
+
startIndex: first.startIndex,
|
|
446
|
+
endIndex: last.endIndex
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const goMergeAdapter = {
|
|
450
|
+
language: "go",
|
|
451
|
+
grammarModule: "tree-sitter-go",
|
|
452
|
+
testFilePatterns: [/_test\.go$/],
|
|
453
|
+
urlFingerprintConfig: GO_URL_FINGERPRINT_CONFIG,
|
|
454
|
+
parseStatements(tree, source) {
|
|
455
|
+
const imports = [];
|
|
456
|
+
const importAnchors = [];
|
|
457
|
+
let importInsertionAnchor;
|
|
458
|
+
const statements = [];
|
|
459
|
+
for (const child of tree.rootNode.children) {
|
|
460
|
+
if (child.type === "comment") continue;
|
|
461
|
+
if (child.type === "package_clause") importInsertionAnchor = source.slice(child.startIndex, child.endIndex);
|
|
462
|
+
if (child.type === "import_declaration") {
|
|
463
|
+
imports.push(...importEntries(child, source));
|
|
464
|
+
importAnchors.push(source.slice(child.startIndex, child.endIndex));
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const key = declarationKey$1(child);
|
|
468
|
+
statements.push({
|
|
469
|
+
key,
|
|
470
|
+
kind: key ? "declaration" : "other",
|
|
471
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
imports,
|
|
476
|
+
importAnchors,
|
|
477
|
+
importInsertionAnchor,
|
|
478
|
+
statements
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
renderImports(imports) {
|
|
482
|
+
if (imports.length === 0) return [];
|
|
483
|
+
if (imports.length === 1) return [`import ${imports[0].text}`];
|
|
484
|
+
return [`import (\n${imports.map((entry) => ` ${entry.text}`).join("\n")}\n)`];
|
|
485
|
+
},
|
|
486
|
+
extractDocstrings(tree, source) {
|
|
487
|
+
const result = /* @__PURE__ */ new Map();
|
|
488
|
+
const rootChildren = tree.rootNode.children;
|
|
489
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
490
|
+
const child = rootChildren[i];
|
|
491
|
+
const key = declarationKey$1(child);
|
|
492
|
+
if (!key || key.startsWith("__package:")) continue;
|
|
493
|
+
const docstring = collectPrecedingGoComments(rootChildren, i, source);
|
|
494
|
+
result.set(key, {
|
|
495
|
+
docstring,
|
|
496
|
+
declStartIndex: child.startIndex,
|
|
497
|
+
declColumn: child.startPosition.column,
|
|
498
|
+
members: /* @__PURE__ */ new Map()
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/engine/merge-adapters/kotlin.ts
|
|
506
|
+
const KOTLIN_URL_FINGERPRINT_CONFIG = {
|
|
507
|
+
stringNodeTypes: ["string_literal"],
|
|
508
|
+
contentNodeTypes: ["string_content"],
|
|
509
|
+
interpolationNodeTypes: ["interpolated_expression"]
|
|
510
|
+
};
|
|
511
|
+
function extractDeclarationName(node) {
|
|
512
|
+
if (node.type === "class_declaration") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
|
|
513
|
+
if (node.type === "function_declaration") return node.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
514
|
+
if (node.type === "object_declaration") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
|
|
515
|
+
if (node.type === "type_alias") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
|
|
516
|
+
if (node.type === "property_declaration") {
|
|
517
|
+
const nameNode = node.children.find((c) => c.type === "variable_declaration");
|
|
518
|
+
if (nameNode) return nameNode.children.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
function findPrecedingKdoc(children, index, source) {
|
|
523
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
524
|
+
const prev = children[k];
|
|
525
|
+
if (prev.type === "multiline_comment") {
|
|
526
|
+
const text = source.slice(prev.startIndex, prev.endIndex);
|
|
527
|
+
if (text.startsWith("/**")) return {
|
|
528
|
+
text,
|
|
529
|
+
startIndex: prev.startIndex,
|
|
530
|
+
endIndex: prev.endIndex
|
|
531
|
+
};
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
if (prev.type === "line_comment") continue;
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
const kotlinMergeAdapter = {
|
|
540
|
+
language: "kotlin",
|
|
541
|
+
grammarModule: "tree-sitter-kotlin",
|
|
542
|
+
testFilePatterns: [/Test\.kt$/],
|
|
543
|
+
urlFingerprintConfig: KOTLIN_URL_FINGERPRINT_CONFIG,
|
|
544
|
+
parseStatements(tree, source) {
|
|
545
|
+
const imports = [];
|
|
546
|
+
const importAnchors = [];
|
|
547
|
+
const statements = [];
|
|
548
|
+
for (const child of tree.rootNode.children) {
|
|
549
|
+
if (child.type === "multiline_comment" || child.type === "line_comment") continue;
|
|
550
|
+
if (child.type === "import_list") {
|
|
551
|
+
const fullText = source.slice(child.startIndex, child.endIndex);
|
|
552
|
+
importAnchors.push(fullText);
|
|
553
|
+
for (const imp of child.namedChildren) if (imp.type === "import_header") {
|
|
554
|
+
const text = source.slice(imp.startIndex, imp.endIndex).trim();
|
|
555
|
+
const key = imp.children.find((c) => c.type === "identifier")?.text ?? text;
|
|
556
|
+
imports.push({
|
|
557
|
+
key,
|
|
558
|
+
text
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const name = extractDeclarationName(child);
|
|
564
|
+
statements.push({
|
|
565
|
+
key: name,
|
|
566
|
+
kind: name ? "declaration" : "other",
|
|
567
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
imports,
|
|
572
|
+
importAnchors,
|
|
573
|
+
statements
|
|
574
|
+
};
|
|
575
|
+
},
|
|
576
|
+
renderImports(imports) {
|
|
577
|
+
return imports.map((entry) => entry.text);
|
|
578
|
+
},
|
|
579
|
+
extractDocstrings(tree, source) {
|
|
580
|
+
const result = /* @__PURE__ */ new Map();
|
|
581
|
+
const rootChildren = tree.rootNode.children;
|
|
582
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
583
|
+
const child = rootChildren[i];
|
|
584
|
+
const name = extractDeclarationName(child);
|
|
585
|
+
if (!name) continue;
|
|
586
|
+
const docstring = findPrecedingKdoc(rootChildren, i, source);
|
|
587
|
+
const members = /* @__PURE__ */ new Map();
|
|
588
|
+
if (child.type === "class_declaration") {
|
|
589
|
+
const body = child.children.find((c) => c.type === "class_body" || c.type === "enum_class_body");
|
|
590
|
+
if (body) {
|
|
591
|
+
const bodyChildren = body.children;
|
|
592
|
+
for (let j = 0; j < bodyChildren.length; j++) {
|
|
593
|
+
const member = bodyChildren[j];
|
|
594
|
+
const memberName = extractDeclarationName(member);
|
|
595
|
+
if (!memberName) continue;
|
|
596
|
+
const memberDoc = findPrecedingKdoc(bodyChildren, j, source);
|
|
597
|
+
members.set(memberName, {
|
|
598
|
+
docstring: memberDoc,
|
|
599
|
+
declStartIndex: member.startIndex,
|
|
600
|
+
declColumn: member.startPosition.column,
|
|
601
|
+
urlFingerprint: extractUrlFingerprint(member, KOTLIN_URL_FINGERPRINT_CONFIG)
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
result.set(name, {
|
|
607
|
+
docstring,
|
|
608
|
+
declStartIndex: child.startIndex,
|
|
609
|
+
declColumn: child.startPosition.column,
|
|
610
|
+
members
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/engine/merge-adapters/node.ts
|
|
618
|
+
const REEXPORT_PREFIX = "__export:";
|
|
619
|
+
function extractDeclName(node) {
|
|
620
|
+
const nameNode = node.childForFieldName("name");
|
|
621
|
+
if (nameNode) return nameNode.text;
|
|
622
|
+
if (node.type === "lexical_declaration") {
|
|
623
|
+
const declarator = node.firstNamedChild;
|
|
624
|
+
if (declarator?.type === "variable_declarator") {
|
|
625
|
+
const name = declarator.childForFieldName("name");
|
|
626
|
+
if (name) return name.text;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
function extractNodeKey(node) {
|
|
632
|
+
if (node.type === "export_statement") {
|
|
633
|
+
const decl = node.childForFieldName("declaration");
|
|
634
|
+
if (decl) return {
|
|
635
|
+
key: extractDeclName(decl),
|
|
636
|
+
kind: "declaration"
|
|
637
|
+
};
|
|
638
|
+
const source = node.childForFieldName("source");
|
|
639
|
+
if (source) return {
|
|
640
|
+
key: `${REEXPORT_PREFIX}${normalizeJsExtension(source.text)}`,
|
|
641
|
+
kind: "reexport"
|
|
642
|
+
};
|
|
643
|
+
return {
|
|
644
|
+
key: null,
|
|
645
|
+
kind: "other"
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
key: extractDeclName(node),
|
|
650
|
+
kind: extractDeclName(node) ? "declaration" : "other"
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function extractClassMembers(classBody, source) {
|
|
654
|
+
const members = [];
|
|
655
|
+
for (const child of classBody.namedChildren) if (child.type === "method_definition") {
|
|
656
|
+
const nameNode = child.childForFieldName("name");
|
|
657
|
+
if (nameNode && nameNode.text !== "constructor") members.push({
|
|
658
|
+
key: nameNode.text,
|
|
659
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
660
|
+
});
|
|
661
|
+
} else if (child.type === "public_field_definition") {
|
|
662
|
+
const nameNode = child.childForFieldName("name");
|
|
663
|
+
if (nameNode) {
|
|
664
|
+
let endIdx = child.endIndex;
|
|
665
|
+
if (source[endIdx] === ";") endIdx += 1;
|
|
666
|
+
members.push({
|
|
667
|
+
key: nameNode.text,
|
|
668
|
+
text: source.slice(child.startIndex, endIdx)
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return members;
|
|
673
|
+
}
|
|
674
|
+
function extractInterfaceMembers(body, source) {
|
|
675
|
+
const members = [];
|
|
676
|
+
for (const child of body.namedChildren) if (child.type === "property_signature" || child.type === "method_signature") {
|
|
677
|
+
const nameNode = child.childForFieldName("name");
|
|
678
|
+
if (nameNode) members.push({
|
|
679
|
+
key: nameNode.text,
|
|
680
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return members;
|
|
684
|
+
}
|
|
685
|
+
function extractEnumMembers(body, source) {
|
|
686
|
+
const members = [];
|
|
687
|
+
for (const child of body.namedChildren) if (child.type === "enum_assignment") {
|
|
688
|
+
const nameNode = child.childForFieldName("name");
|
|
689
|
+
if (nameNode) members.push({
|
|
690
|
+
key: nameNode.text,
|
|
691
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
692
|
+
});
|
|
693
|
+
} else if (child.type === "property_identifier") members.push({
|
|
694
|
+
key: child.text,
|
|
695
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
696
|
+
});
|
|
697
|
+
return members;
|
|
698
|
+
}
|
|
699
|
+
function isCommentNode(node) {
|
|
700
|
+
return node.type === "comment" || node.grammarType === "comment";
|
|
701
|
+
}
|
|
702
|
+
function findPrecedingDocstring(children, index, source) {
|
|
703
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
704
|
+
const prev = children[k];
|
|
705
|
+
if (isCommentNode(prev)) {
|
|
706
|
+
const text = source.slice(prev.startIndex, prev.endIndex);
|
|
707
|
+
if (text.startsWith("/**")) return {
|
|
708
|
+
text,
|
|
709
|
+
startIndex: prev.startIndex,
|
|
710
|
+
endIndex: prev.endIndex
|
|
711
|
+
};
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
if (prev.type === "," || prev.type === ";" || prev.type === "{") continue;
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
const CLASS_MEMBER_TYPES = new Set(["method_definition", "public_field_definition"]);
|
|
720
|
+
const INTERFACE_MEMBER_TYPES = new Set(["property_signature", "method_signature"]);
|
|
721
|
+
const ENUM_MEMBER_TYPES = new Set(["enum_assignment", "property_identifier"]);
|
|
722
|
+
const NODE_URL_FINGERPRINT_CONFIG = {
|
|
723
|
+
stringNodeTypes: ["string", "template_string"],
|
|
724
|
+
contentNodeTypes: ["string_fragment"],
|
|
725
|
+
interpolationNodeTypes: ["template_substitution"]
|
|
726
|
+
};
|
|
727
|
+
function extractBodyMemberDocstrings(body, source, memberTypes) {
|
|
728
|
+
const result = /* @__PURE__ */ new Map();
|
|
729
|
+
const allChildren = body.children;
|
|
730
|
+
for (let i = 0; i < allChildren.length; i++) {
|
|
731
|
+
const child = allChildren[i];
|
|
732
|
+
if (!memberTypes.has(child.type)) continue;
|
|
733
|
+
let memberName = null;
|
|
734
|
+
if (child.type === "property_identifier") memberName = child.text;
|
|
735
|
+
else {
|
|
736
|
+
const nameNode = child.childForFieldName("name");
|
|
737
|
+
if (nameNode) memberName = nameNode.text;
|
|
738
|
+
}
|
|
739
|
+
if (!memberName || memberName === "constructor") continue;
|
|
740
|
+
const docstring = findPrecedingDocstring(allChildren, i, source);
|
|
741
|
+
const fp = extractUrlFingerprint(child, NODE_URL_FINGERPRINT_CONFIG);
|
|
742
|
+
result.set(memberName, {
|
|
743
|
+
docstring,
|
|
744
|
+
declStartIndex: child.startIndex,
|
|
745
|
+
declColumn: child.startPosition.column,
|
|
746
|
+
urlFingerprint: fp
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
const nodeMergeAdapter = {
|
|
752
|
+
language: "node",
|
|
753
|
+
grammarModule: "tree-sitter-typescript/bindings/node/typescript.js",
|
|
754
|
+
normalizeReexport: normalizeJsExtension,
|
|
755
|
+
testFilePatterns: [/\.(spec|test)\.[jt]sx?$/],
|
|
756
|
+
urlFingerprintConfig: NODE_URL_FINGERPRINT_CONFIG,
|
|
757
|
+
shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
|
|
758
|
+
const newText = newMembers.map((m) => m.text).join("\n");
|
|
759
|
+
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
760
|
+
const propName = match[1];
|
|
761
|
+
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
762
|
+
}
|
|
763
|
+
return false;
|
|
764
|
+
},
|
|
765
|
+
parseStatements(tree, source) {
|
|
766
|
+
const imports = [];
|
|
767
|
+
const importAnchors = [];
|
|
768
|
+
const statements = [];
|
|
769
|
+
for (const child of tree.rootNode.children) {
|
|
770
|
+
if (child.type === "comment") continue;
|
|
771
|
+
if (child.type === "import_statement") {
|
|
772
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
773
|
+
const sourceNode = child.childForFieldName("source");
|
|
774
|
+
const modulePath = sourceNode ? normalizeJsExtension(sourceNode.text) : normalizeJsExtension(text.trim());
|
|
775
|
+
imports.push({
|
|
776
|
+
key: modulePath,
|
|
777
|
+
text
|
|
778
|
+
});
|
|
779
|
+
importAnchors.push(text);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const { key, kind } = extractNodeKey(child);
|
|
783
|
+
statements.push({
|
|
784
|
+
key,
|
|
785
|
+
kind,
|
|
786
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
imports,
|
|
791
|
+
importAnchors,
|
|
792
|
+
statements
|
|
793
|
+
};
|
|
794
|
+
},
|
|
795
|
+
renderImports(imports) {
|
|
796
|
+
return imports.map((entry) => entry.text);
|
|
797
|
+
},
|
|
798
|
+
extractMembers(tree, source) {
|
|
799
|
+
const result = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const child of tree.rootNode.children) {
|
|
801
|
+
if (child.type !== "export_statement") continue;
|
|
802
|
+
const decl = child.childForFieldName("declaration");
|
|
803
|
+
if (!decl) continue;
|
|
804
|
+
const nameNode = decl.childForFieldName("name");
|
|
805
|
+
if (!nameNode) continue;
|
|
806
|
+
const symbolName = nameNode.text;
|
|
807
|
+
if (decl.type === "class_declaration") {
|
|
808
|
+
const body = decl.childForFieldName("body");
|
|
809
|
+
if (!body) continue;
|
|
810
|
+
const members = extractClassMembers(body, source);
|
|
811
|
+
const firstMember = body.firstNamedChild;
|
|
812
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
813
|
+
result.set(symbolName, {
|
|
814
|
+
members,
|
|
815
|
+
bodyEndLine: body.endPosition.row,
|
|
816
|
+
memberIndent
|
|
817
|
+
});
|
|
818
|
+
} else if (decl.type === "interface_declaration") {
|
|
819
|
+
const body = decl.childForFieldName("body");
|
|
820
|
+
if (!body) continue;
|
|
821
|
+
const members = extractInterfaceMembers(body, source);
|
|
822
|
+
const firstMember = body.firstNamedChild;
|
|
823
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
824
|
+
result.set(symbolName, {
|
|
825
|
+
members,
|
|
826
|
+
bodyEndLine: body.endPosition.row,
|
|
827
|
+
memberIndent
|
|
828
|
+
});
|
|
829
|
+
} else if (decl.type === "enum_declaration") {
|
|
830
|
+
const body = decl.childForFieldName("body");
|
|
831
|
+
if (!body) continue;
|
|
832
|
+
const members = extractEnumMembers(body, source);
|
|
833
|
+
const firstMember = body.firstNamedChild;
|
|
834
|
+
const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
|
|
835
|
+
result.set(symbolName, {
|
|
836
|
+
members,
|
|
837
|
+
bodyEndLine: body.endPosition.row,
|
|
838
|
+
memberIndent
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return result;
|
|
843
|
+
},
|
|
844
|
+
extractDocstrings(tree, source) {
|
|
845
|
+
const result = /* @__PURE__ */ new Map();
|
|
846
|
+
const rootChildren = tree.rootNode.children;
|
|
847
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
848
|
+
const child = rootChildren[i];
|
|
849
|
+
if (child.type !== "export_statement") continue;
|
|
850
|
+
const decl = child.childForFieldName("declaration");
|
|
851
|
+
if (!decl) continue;
|
|
852
|
+
const nameNode = decl.childForFieldName("name");
|
|
853
|
+
if (!nameNode) continue;
|
|
854
|
+
const docstring = findPrecedingDocstring(rootChildren, i, source);
|
|
855
|
+
let members = /* @__PURE__ */ new Map();
|
|
856
|
+
const body = decl.childForFieldName("body");
|
|
857
|
+
if (body) {
|
|
858
|
+
if (decl.type === "class_declaration") members = extractBodyMemberDocstrings(body, source, CLASS_MEMBER_TYPES);
|
|
859
|
+
else if (decl.type === "interface_declaration") members = extractBodyMemberDocstrings(body, source, INTERFACE_MEMBER_TYPES);
|
|
860
|
+
else if (decl.type === "enum_declaration") members = extractBodyMemberDocstrings(body, source, ENUM_MEMBER_TYPES);
|
|
861
|
+
}
|
|
862
|
+
result.set(nameNode.text, {
|
|
863
|
+
docstring,
|
|
864
|
+
declStartIndex: child.startIndex,
|
|
865
|
+
declColumn: child.startPosition.column,
|
|
866
|
+
members
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/engine/merge-adapters/php.ts
|
|
874
|
+
const PHP_URL_FINGERPRINT_CONFIG = {
|
|
875
|
+
stringNodeTypes: ["encapsed_string", "string"],
|
|
876
|
+
contentNodeTypes: ["string_content"],
|
|
877
|
+
interpolationNodeTypes: ["variable_name"]
|
|
878
|
+
};
|
|
879
|
+
function extractPhpDeclarationName(node) {
|
|
880
|
+
switch (node.type) {
|
|
881
|
+
case "class_declaration":
|
|
882
|
+
case "interface_declaration":
|
|
883
|
+
case "trait_declaration":
|
|
884
|
+
case "enum_declaration":
|
|
885
|
+
case "function_definition": return node.childForFieldName("name")?.text ?? null;
|
|
886
|
+
case "namespace_definition": {
|
|
887
|
+
const nameNode = node.childForFieldName("name");
|
|
888
|
+
return nameNode ? `__namespace:${nameNode.text}` : "__namespace:";
|
|
889
|
+
}
|
|
890
|
+
default: return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function findPhpDocstring(children, index, source) {
|
|
894
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
895
|
+
const prev = children[k];
|
|
896
|
+
if (prev.type === "comment") {
|
|
897
|
+
const text = source.slice(prev.startIndex, prev.endIndex);
|
|
898
|
+
if (text.startsWith("/**")) return {
|
|
899
|
+
text,
|
|
900
|
+
startIndex: prev.startIndex,
|
|
901
|
+
endIndex: prev.endIndex
|
|
902
|
+
};
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
if (prev.type === ";" || prev.type === "{") continue;
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
const phpMergeAdapter = {
|
|
911
|
+
language: "php",
|
|
912
|
+
grammarModule: "tree-sitter-php",
|
|
913
|
+
testFilePatterns: [/Test\.php$/],
|
|
914
|
+
urlFingerprintConfig: PHP_URL_FINGERPRINT_CONFIG,
|
|
915
|
+
resolveGrammar: (mod) => {
|
|
916
|
+
if (typeof mod === "object" && mod !== null && "php" in mod) return mod.php;
|
|
917
|
+
return mod;
|
|
918
|
+
},
|
|
919
|
+
parseStatements(tree, source) {
|
|
920
|
+
const imports = [];
|
|
921
|
+
const importAnchors = [];
|
|
922
|
+
let importInsertionAnchor;
|
|
923
|
+
const statements = [];
|
|
924
|
+
for (const child of tree.rootNode.children) {
|
|
925
|
+
if (child.type === "comment") continue;
|
|
926
|
+
if (child.type === "namespace_definition") importInsertionAnchor = source.slice(child.startIndex, child.endIndex);
|
|
927
|
+
if (child.type === "namespace_use_declaration") {
|
|
928
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
929
|
+
imports.push({
|
|
930
|
+
key: text.trim(),
|
|
931
|
+
text
|
|
932
|
+
});
|
|
933
|
+
importAnchors.push(text);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const declarationName = extractPhpDeclarationName(child);
|
|
937
|
+
const kind = declarationName ? "declaration" : "other";
|
|
938
|
+
statements.push({
|
|
939
|
+
key: declarationName,
|
|
940
|
+
kind,
|
|
941
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
imports,
|
|
946
|
+
importAnchors,
|
|
947
|
+
importInsertionAnchor,
|
|
948
|
+
statements
|
|
949
|
+
};
|
|
950
|
+
},
|
|
951
|
+
renderImports(imports) {
|
|
952
|
+
return imports.map((entry) => entry.text);
|
|
953
|
+
},
|
|
954
|
+
extractDocstrings(tree, source) {
|
|
955
|
+
const result = /* @__PURE__ */ new Map();
|
|
956
|
+
const rootChildren = tree.rootNode.children;
|
|
957
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
958
|
+
const child = rootChildren[i];
|
|
959
|
+
const name = extractPhpDeclarationName(child);
|
|
960
|
+
if (!name || name.startsWith("__namespace:")) continue;
|
|
961
|
+
const docstring = findPhpDocstring(rootChildren, i, source);
|
|
962
|
+
const members = /* @__PURE__ */ new Map();
|
|
963
|
+
const body = child.childForFieldName("body");
|
|
964
|
+
if (body) {
|
|
965
|
+
const bodyChildren = body.children;
|
|
966
|
+
for (let j = 0; j < bodyChildren.length; j++) {
|
|
967
|
+
const member = bodyChildren[j];
|
|
968
|
+
if (member.type !== "method_declaration") continue;
|
|
969
|
+
const memberName = member.childForFieldName("name")?.text;
|
|
970
|
+
if (!memberName) continue;
|
|
971
|
+
const memberDoc = findPhpDocstring(bodyChildren, j, source);
|
|
972
|
+
members.set(memberName, {
|
|
973
|
+
docstring: memberDoc,
|
|
974
|
+
declStartIndex: member.startIndex,
|
|
975
|
+
declColumn: member.startPosition.column,
|
|
976
|
+
urlFingerprint: extractUrlFingerprint(member, PHP_URL_FINGERPRINT_CONFIG)
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
result.set(name, {
|
|
981
|
+
docstring,
|
|
982
|
+
declStartIndex: child.startIndex,
|
|
983
|
+
declColumn: child.startPosition.column,
|
|
984
|
+
members
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/engine/merge-adapters/python.ts
|
|
992
|
+
const PYTHON_URL_FINGERPRINT_CONFIG = {
|
|
993
|
+
stringNodeTypes: ["string"],
|
|
994
|
+
contentNodeTypes: ["string_content"],
|
|
995
|
+
interpolationNodeTypes: ["interpolation"]
|
|
996
|
+
};
|
|
997
|
+
function extractPythonDeclarationName(node) {
|
|
998
|
+
if (node.type === "class_definition" || node.type === "function_definition") return node.childForFieldName("name")?.text ?? null;
|
|
999
|
+
if (node.type === "expression_statement") {
|
|
1000
|
+
const firstChild = node.firstNamedChild;
|
|
1001
|
+
if (firstChild?.type === "assignment") {
|
|
1002
|
+
const left = firstChild.childForFieldName("left");
|
|
1003
|
+
if (left?.type === "identifier" && left.text === "__all__") return "__all__";
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
function extractPythonDocstring(bodyNode, source) {
|
|
1009
|
+
for (const child of bodyNode.namedChildren) {
|
|
1010
|
+
if (child.type === "expression_statement") {
|
|
1011
|
+
if (child.firstNamedChild?.type === "string") return {
|
|
1012
|
+
text: source.slice(child.startIndex, child.endIndex),
|
|
1013
|
+
startIndex: child.startIndex,
|
|
1014
|
+
endIndex: child.endIndex
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
function collectPrecedingPythonComments(children, index, source) {
|
|
1022
|
+
let lastIdx = -1;
|
|
1023
|
+
let firstIdx = -1;
|
|
1024
|
+
for (let k = index - 1; k >= 0; k--) if (children[k].type === "comment") {
|
|
1025
|
+
if (lastIdx === -1) lastIdx = k;
|
|
1026
|
+
firstIdx = k;
|
|
1027
|
+
} else break;
|
|
1028
|
+
if (firstIdx === -1) return null;
|
|
1029
|
+
const first = children[firstIdx];
|
|
1030
|
+
const last = children[lastIdx];
|
|
1031
|
+
return {
|
|
1032
|
+
text: source.slice(first.startIndex, last.endIndex),
|
|
1033
|
+
startIndex: first.startIndex,
|
|
1034
|
+
endIndex: last.endIndex
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const pythonMergeAdapter = {
|
|
1038
|
+
language: "python",
|
|
1039
|
+
grammarModule: "tree-sitter-python",
|
|
1040
|
+
testFilePatterns: [/(?:^|\/)test_.*\.py$/, /_test\.py$/],
|
|
1041
|
+
urlFingerprintConfig: PYTHON_URL_FINGERPRINT_CONFIG,
|
|
1042
|
+
parseStatements(tree, source) {
|
|
1043
|
+
const imports = [];
|
|
1044
|
+
const importAnchors = [];
|
|
1045
|
+
const statements = [];
|
|
1046
|
+
for (const child of tree.rootNode.children) {
|
|
1047
|
+
if (child.type === "comment") continue;
|
|
1048
|
+
if (child.type === "import_statement" || child.type === "import_from_statement") {
|
|
1049
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
1050
|
+
imports.push({
|
|
1051
|
+
key: text.trim(),
|
|
1052
|
+
text
|
|
1053
|
+
});
|
|
1054
|
+
importAnchors.push(text);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
const declarationName = extractPythonDeclarationName(child);
|
|
1058
|
+
const kind = declarationName ? "declaration" : "other";
|
|
1059
|
+
statements.push({
|
|
1060
|
+
key: declarationName,
|
|
1061
|
+
kind,
|
|
1062
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
imports,
|
|
1067
|
+
importAnchors,
|
|
1068
|
+
statements
|
|
1069
|
+
};
|
|
1070
|
+
},
|
|
1071
|
+
renderImports(imports) {
|
|
1072
|
+
return imports.map((entry) => entry.text);
|
|
1073
|
+
},
|
|
1074
|
+
extractDocstrings(tree, source) {
|
|
1075
|
+
const result = /* @__PURE__ */ new Map();
|
|
1076
|
+
const rootChildren = tree.rootNode.children;
|
|
1077
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
1078
|
+
const child = rootChildren[i];
|
|
1079
|
+
const name = extractPythonDeclarationName(child);
|
|
1080
|
+
if (!name) continue;
|
|
1081
|
+
let docstring = null;
|
|
1082
|
+
const body = child.childForFieldName("body");
|
|
1083
|
+
if (body) docstring = extractPythonDocstring(body, source);
|
|
1084
|
+
if (!docstring) docstring = collectPrecedingPythonComments(rootChildren, i, source);
|
|
1085
|
+
const members = /* @__PURE__ */ new Map();
|
|
1086
|
+
if (child.type === "class_definition" && body) for (const member of body.namedChildren) {
|
|
1087
|
+
if (member.type !== "function_definition") continue;
|
|
1088
|
+
const memberName = member.childForFieldName("name")?.text;
|
|
1089
|
+
if (!memberName) continue;
|
|
1090
|
+
let memberDoc = null;
|
|
1091
|
+
const memberBody = member.childForFieldName("body");
|
|
1092
|
+
if (memberBody) memberDoc = extractPythonDocstring(memberBody, source);
|
|
1093
|
+
members.set(memberName, {
|
|
1094
|
+
docstring: memberDoc,
|
|
1095
|
+
declStartIndex: member.startIndex,
|
|
1096
|
+
declColumn: member.startPosition.column,
|
|
1097
|
+
urlFingerprint: extractUrlFingerprint(member, PYTHON_URL_FINGERPRINT_CONFIG)
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
result.set(name, {
|
|
1101
|
+
docstring,
|
|
1102
|
+
declStartIndex: child.startIndex,
|
|
1103
|
+
declColumn: child.startPosition.column,
|
|
1104
|
+
members
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return result;
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/engine/merge-adapters/ruby.ts
|
|
1112
|
+
const RUBY_URL_FINGERPRINT_CONFIG = {
|
|
1113
|
+
stringNodeTypes: ["string"],
|
|
1114
|
+
contentNodeTypes: ["string_content"],
|
|
1115
|
+
interpolationNodeTypes: ["interpolation"]
|
|
1116
|
+
};
|
|
1117
|
+
function isRequireCall(node) {
|
|
1118
|
+
if (node.type !== "call") return false;
|
|
1119
|
+
const method = node.childForFieldName("method");
|
|
1120
|
+
if (!method) return false;
|
|
1121
|
+
return method.text === "require" || method.text === "require_relative";
|
|
1122
|
+
}
|
|
1123
|
+
function extractRubyDeclarationName(node) {
|
|
1124
|
+
if (node.type !== "class" && node.type !== "module") return null;
|
|
1125
|
+
return node.childForFieldName("name")?.text ?? null;
|
|
1126
|
+
}
|
|
1127
|
+
function collectPrecedingRubyComments(children, index, source) {
|
|
1128
|
+
let lastIdx = -1;
|
|
1129
|
+
let firstIdx = -1;
|
|
1130
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
1131
|
+
const prev = children[k];
|
|
1132
|
+
if ((prev.grammarType ?? prev.type) === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("#")) {
|
|
1133
|
+
if (lastIdx === -1) lastIdx = k;
|
|
1134
|
+
firstIdx = k;
|
|
1135
|
+
} else break;
|
|
1136
|
+
else break;
|
|
1137
|
+
}
|
|
1138
|
+
if (firstIdx === -1) return null;
|
|
1139
|
+
const first = children[firstIdx];
|
|
1140
|
+
const last = children[lastIdx];
|
|
1141
|
+
return {
|
|
1142
|
+
text: source.slice(first.startIndex, last.endIndex),
|
|
1143
|
+
startIndex: first.startIndex,
|
|
1144
|
+
endIndex: last.endIndex
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const rubyMergeAdapter = {
|
|
1148
|
+
language: "ruby",
|
|
1149
|
+
grammarModule: "tree-sitter-ruby",
|
|
1150
|
+
testFilePatterns: [/_test\.rb$/, /_spec\.rb$/],
|
|
1151
|
+
urlFingerprintConfig: RUBY_URL_FINGERPRINT_CONFIG,
|
|
1152
|
+
parseStatements(tree, source) {
|
|
1153
|
+
const imports = [];
|
|
1154
|
+
const importAnchors = [];
|
|
1155
|
+
const statements = [];
|
|
1156
|
+
for (const child of tree.rootNode.children) {
|
|
1157
|
+
if (child.type === "comment") continue;
|
|
1158
|
+
const declarationName = extractRubyDeclarationName(child);
|
|
1159
|
+
if (isRequireCall(child)) {
|
|
1160
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
1161
|
+
imports.push({
|
|
1162
|
+
key: text.trim(),
|
|
1163
|
+
text
|
|
1164
|
+
});
|
|
1165
|
+
importAnchors.push(text);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
const kind = declarationName ? "declaration" : "other";
|
|
1169
|
+
statements.push({
|
|
1170
|
+
key: declarationName,
|
|
1171
|
+
kind,
|
|
1172
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
imports,
|
|
1177
|
+
importAnchors,
|
|
1178
|
+
statements
|
|
1179
|
+
};
|
|
1180
|
+
},
|
|
1181
|
+
renderImports(imports) {
|
|
1182
|
+
return imports.map((entry) => entry.text);
|
|
1183
|
+
},
|
|
1184
|
+
extractDocstrings(tree, source) {
|
|
1185
|
+
const result = /* @__PURE__ */ new Map();
|
|
1186
|
+
const rootChildren = tree.rootNode.children;
|
|
1187
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
1188
|
+
const child = rootChildren[i];
|
|
1189
|
+
const name = extractRubyDeclarationName(child);
|
|
1190
|
+
if (!name) continue;
|
|
1191
|
+
const docstring = collectPrecedingRubyComments(rootChildren, i, source);
|
|
1192
|
+
const members = /* @__PURE__ */ new Map();
|
|
1193
|
+
if (child.type === "class" || child.type === "module") {
|
|
1194
|
+
const allClassChildren = child.children;
|
|
1195
|
+
const body = child.childForFieldName("body");
|
|
1196
|
+
let firstMethodDocFromClass = null;
|
|
1197
|
+
for (let j = 0; j < allClassChildren.length; j++) if (allClassChildren[j] === body || allClassChildren[j].type === "body_statement") {
|
|
1198
|
+
firstMethodDocFromClass = collectPrecedingRubyComments(allClassChildren, j, source);
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
if (body) {
|
|
1202
|
+
const bodyChildren = body.children;
|
|
1203
|
+
let firstMethodHandled = false;
|
|
1204
|
+
for (let j = 0; j < bodyChildren.length; j++) {
|
|
1205
|
+
const member = bodyChildren[j];
|
|
1206
|
+
if (member.type !== "method") continue;
|
|
1207
|
+
const memberName = member.childForFieldName("name")?.text;
|
|
1208
|
+
if (!memberName) continue;
|
|
1209
|
+
let memberDoc;
|
|
1210
|
+
if (!firstMethodHandled && firstMethodDocFromClass) {
|
|
1211
|
+
memberDoc = firstMethodDocFromClass;
|
|
1212
|
+
firstMethodHandled = true;
|
|
1213
|
+
} else {
|
|
1214
|
+
memberDoc = collectPrecedingRubyComments(bodyChildren, j, source);
|
|
1215
|
+
firstMethodHandled = true;
|
|
1216
|
+
}
|
|
1217
|
+
members.set(memberName, {
|
|
1218
|
+
docstring: memberDoc,
|
|
1219
|
+
declStartIndex: member.startIndex,
|
|
1220
|
+
declColumn: member.startPosition.column,
|
|
1221
|
+
urlFingerprint: extractUrlFingerprint(member, RUBY_URL_FINGERPRINT_CONFIG)
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
result.set(name, {
|
|
1227
|
+
docstring,
|
|
1228
|
+
declStartIndex: child.startIndex,
|
|
1229
|
+
declColumn: child.startPosition.column,
|
|
1230
|
+
members
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
return result;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/engine/merge-adapters/rust.ts
|
|
1238
|
+
const RUST_URL_FINGERPRINT_CONFIG = {
|
|
1239
|
+
stringNodeTypes: ["string_literal"],
|
|
1240
|
+
contentNodeTypes: ["string_content"],
|
|
1241
|
+
interpolationNodeTypes: []
|
|
1242
|
+
};
|
|
1243
|
+
function declarationKey(node) {
|
|
1244
|
+
switch (node.type) {
|
|
1245
|
+
case "struct_item":
|
|
1246
|
+
case "enum_item":
|
|
1247
|
+
case "function_item":
|
|
1248
|
+
case "trait_item":
|
|
1249
|
+
case "type_item":
|
|
1250
|
+
case "const_item": return node.childForFieldName("name")?.text ?? null;
|
|
1251
|
+
case "impl_item": {
|
|
1252
|
+
const typeName = node.childForFieldName("type")?.text ?? "unknown";
|
|
1253
|
+
const traitName = node.childForFieldName("trait")?.text;
|
|
1254
|
+
return traitName ? `impl:${traitName}->${typeName}` : `impl:${typeName}`;
|
|
1255
|
+
}
|
|
1256
|
+
default: return null;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function collectPrecedingRustDocComments(children, index, source) {
|
|
1260
|
+
let lastIdx = -1;
|
|
1261
|
+
let firstIdx = -1;
|
|
1262
|
+
for (let k = index - 1; k >= 0; k--) {
|
|
1263
|
+
const prev = children[k];
|
|
1264
|
+
if (prev.type === "line_comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
|
|
1265
|
+
if (lastIdx === -1) lastIdx = k;
|
|
1266
|
+
firstIdx = k;
|
|
1267
|
+
} else break;
|
|
1268
|
+
else break;
|
|
1269
|
+
}
|
|
1270
|
+
if (firstIdx === -1) return null;
|
|
1271
|
+
const first = children[firstIdx];
|
|
1272
|
+
const last = children[lastIdx];
|
|
1273
|
+
return {
|
|
1274
|
+
text: source.slice(first.startIndex, last.endIndex),
|
|
1275
|
+
startIndex: first.startIndex,
|
|
1276
|
+
endIndex: last.endIndex
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/engine/merge-adapters/index.ts
|
|
1281
|
+
const adapters = new Map([
|
|
1282
|
+
["dotnet", dotnetMergeAdapter],
|
|
1283
|
+
["elixir", elixirMergeAdapter],
|
|
1284
|
+
["go", goMergeAdapter],
|
|
1285
|
+
["kotlin", kotlinMergeAdapter],
|
|
1286
|
+
["node", nodeMergeAdapter],
|
|
1287
|
+
["php", phpMergeAdapter],
|
|
1288
|
+
["python", pythonMergeAdapter],
|
|
1289
|
+
["ruby", rubyMergeAdapter],
|
|
1290
|
+
["rust", {
|
|
1291
|
+
language: "rust",
|
|
1292
|
+
grammarModule: "tree-sitter-rust",
|
|
1293
|
+
testFilePatterns: [/_test\.rs$/, /(?:^|\/)tests\/.*\.rs$/],
|
|
1294
|
+
urlFingerprintConfig: RUST_URL_FINGERPRINT_CONFIG,
|
|
1295
|
+
parseStatements(tree, source) {
|
|
1296
|
+
const imports = [];
|
|
1297
|
+
const importAnchors = [];
|
|
1298
|
+
const statements = [];
|
|
1299
|
+
for (const child of tree.rootNode.children) {
|
|
1300
|
+
if (child.type === "comment") continue;
|
|
1301
|
+
if (child.type === "use_declaration") {
|
|
1302
|
+
const text = source.slice(child.startIndex, child.endIndex);
|
|
1303
|
+
imports.push({
|
|
1304
|
+
key: text.trim(),
|
|
1305
|
+
text
|
|
1306
|
+
});
|
|
1307
|
+
importAnchors.push(text);
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const key = declarationKey(child);
|
|
1311
|
+
statements.push({
|
|
1312
|
+
key,
|
|
1313
|
+
kind: key ? "declaration" : "other",
|
|
1314
|
+
text: source.slice(child.startIndex, child.endIndex)
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
imports,
|
|
1319
|
+
importAnchors,
|
|
1320
|
+
statements
|
|
1321
|
+
};
|
|
1322
|
+
},
|
|
1323
|
+
renderImports(imports) {
|
|
1324
|
+
return imports.map((entry) => entry.text);
|
|
1325
|
+
},
|
|
1326
|
+
extractDocstrings(tree, source) {
|
|
1327
|
+
const result = /* @__PURE__ */ new Map();
|
|
1328
|
+
const rootChildren = tree.rootNode.children;
|
|
1329
|
+
for (let i = 0; i < rootChildren.length; i++) {
|
|
1330
|
+
const child = rootChildren[i];
|
|
1331
|
+
const key = declarationKey(child);
|
|
1332
|
+
if (!key) continue;
|
|
1333
|
+
const docstring = collectPrecedingRustDocComments(rootChildren, i, source);
|
|
1334
|
+
result.set(key, {
|
|
1335
|
+
docstring,
|
|
1336
|
+
declStartIndex: child.startIndex,
|
|
1337
|
+
declColumn: child.startPosition.column,
|
|
1338
|
+
members: /* @__PURE__ */ new Map()
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
1343
|
+
}]
|
|
1344
|
+
]);
|
|
1345
|
+
function getMergeAdapter(language) {
|
|
1346
|
+
return adapters.get(language);
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/engine/merger.ts
|
|
1350
|
+
/**
|
|
1351
|
+
* AST-level file merger using tree-sitter.
|
|
1352
|
+
*
|
|
1353
|
+
* When generating into a directory that already contains files, the merger
|
|
1354
|
+
* parses both the existing and generated content, detects top-level symbols,
|
|
1355
|
+
* and only appends symbols that don't already exist. Hand-written code is
|
|
1356
|
+
* never modified or removed.
|
|
1357
|
+
*
|
|
1358
|
+
* Supports any language with a tree-sitter grammar package.
|
|
1359
|
+
*/
|
|
1360
|
+
const parserCache = /* @__PURE__ */ new Map();
|
|
1361
|
+
function findIgnoredRegions(source) {
|
|
1362
|
+
const regions = [];
|
|
1363
|
+
const startTag = "@oagen-ignore-start";
|
|
1364
|
+
const endTag = "@oagen-ignore-end";
|
|
1365
|
+
let searchFrom = 0;
|
|
1366
|
+
while (true) {
|
|
1367
|
+
const startIdx = source.indexOf(startTag, searchFrom);
|
|
1368
|
+
if (startIdx === -1) break;
|
|
1369
|
+
const endIdx = source.indexOf(endTag, startIdx + 19);
|
|
1370
|
+
if (endIdx === -1) break;
|
|
1371
|
+
regions.push({
|
|
1372
|
+
startIndex: startIdx,
|
|
1373
|
+
endIndex: endIdx + 17
|
|
1374
|
+
});
|
|
1375
|
+
searchFrom = endIdx + 17;
|
|
1376
|
+
}
|
|
1377
|
+
return regions;
|
|
1378
|
+
}
|
|
1379
|
+
function buildIgnoredSymbolNames(docstrings, regions) {
|
|
1380
|
+
if (regions.length === 0) return /* @__PURE__ */ new Set();
|
|
1381
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
1382
|
+
for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
|
|
1383
|
+
return ignored;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Check if a tree-sitter grammar is configured for the given language.
|
|
1387
|
+
*/
|
|
1388
|
+
function hasGrammar(language) {
|
|
1389
|
+
return getMergeAdapter(language) !== void 0;
|
|
1390
|
+
}
|
|
1391
|
+
async function getParser(language) {
|
|
1392
|
+
const cached = parserCache.get(language);
|
|
1393
|
+
if (cached) return cached;
|
|
1394
|
+
const adapter = getMergeAdapter(language);
|
|
1395
|
+
if (!adapter) throw new Error(`No tree-sitter grammar configured for language "${language}". Add a merge adapter and install the corresponding tree-sitter grammar package.`);
|
|
1396
|
+
const mod = await import(adapter.grammarModule);
|
|
1397
|
+
const exported = adapter.resolveGrammar ? adapter.resolveGrammar(mod) : mod;
|
|
1398
|
+
const grammar = exported.default ?? exported;
|
|
1399
|
+
const parser = new Parser();
|
|
1400
|
+
parser.setLanguage(grammar);
|
|
1401
|
+
parserCache.set(language, parser);
|
|
1402
|
+
return parser;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Extract top-level statements from generated source with their names
|
|
1406
|
+
* and exact text span.
|
|
1407
|
+
*/
|
|
1408
|
+
async function extractStatements(source, language) {
|
|
1409
|
+
const parser = await getParser(language);
|
|
1410
|
+
const adapter = getMergeAdapter(language);
|
|
1411
|
+
if (!adapter) throw new Error(`No merge adapter configured for language "${language}"`);
|
|
1412
|
+
const tree = safeParse(parser, source);
|
|
1413
|
+
return adapter.parseStatements(tree, source);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Merge generated content into an existing file.
|
|
1417
|
+
*
|
|
1418
|
+
* - Parses both files with tree-sitter
|
|
1419
|
+
* - Detects which top-level symbols already exist
|
|
1420
|
+
* - Appends only new symbols
|
|
1421
|
+
* - Never modifies or removes existing code
|
|
1422
|
+
* - Adds auto-generated header if not present
|
|
1423
|
+
*/
|
|
1424
|
+
async function mergeIntoExisting(existingContent, generatedContent, language, header) {
|
|
1425
|
+
const adapter = getMergeAdapter(language);
|
|
1426
|
+
if (!adapter) throw new Error(`No merge adapter configured for language "${language}"`);
|
|
1427
|
+
const existingStatements = await extractStatements(existingContent, language);
|
|
1428
|
+
const generatedStatements = await extractStatements(generatedContent, language);
|
|
1429
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
1430
|
+
const existingUnnamedTexts = /* @__PURE__ */ new Set();
|
|
1431
|
+
const existingImportKeys = /* @__PURE__ */ new Set();
|
|
1432
|
+
const existingReexports = /* @__PURE__ */ new Set();
|
|
1433
|
+
let lastImportEndIndex = -1;
|
|
1434
|
+
const existingImportedNames = /* @__PURE__ */ new Set();
|
|
1435
|
+
for (const imp of existingStatements.imports) {
|
|
1436
|
+
existingImportKeys.add(imp.key);
|
|
1437
|
+
const braceMatch = imp.text.match(/\{([^}]+)\}/);
|
|
1438
|
+
if (braceMatch) for (const name of braceMatch[1].split(",")) {
|
|
1439
|
+
const trimmed = name.replace(/\btype\b/, "").trim();
|
|
1440
|
+
if (trimmed) existingImportedNames.add(trimmed);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
for (const anchor of existingStatements.importAnchors) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(anchor)).split("\n").length - 1 + anchor.split("\n").length - 1;
|
|
1444
|
+
if (lastImportEndIndex === -1 && existingStatements.importInsertionAnchor) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(existingStatements.importInsertionAnchor)).split("\n").length - 1 + existingStatements.importInsertionAnchor.split("\n").length - 1;
|
|
1445
|
+
for (const stmt of existingStatements.statements) {
|
|
1446
|
+
if (stmt.kind === "reexport") existingReexports.add(adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim());
|
|
1447
|
+
if (stmt.key) existingKeys.add(stmt.key);
|
|
1448
|
+
else existingUnnamedTexts.add(stmt.text.trim());
|
|
1449
|
+
}
|
|
1450
|
+
const headerLine = header.trim();
|
|
1451
|
+
const newImports = [];
|
|
1452
|
+
const toAppend = [];
|
|
1453
|
+
let preserved = 0;
|
|
1454
|
+
for (const imp of generatedStatements.imports) {
|
|
1455
|
+
if (imp.text.trim() === headerLine) continue;
|
|
1456
|
+
if (existingImportKeys.has(imp.key)) {
|
|
1457
|
+
const braceMatch = imp.text.match(/\{([^}]+)\}/);
|
|
1458
|
+
if (braceMatch) {
|
|
1459
|
+
const newNames = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean).filter((n) => !existingImportedNames.has(n) && !existingKeys.has(n));
|
|
1460
|
+
if (newNames.length > 0) {
|
|
1461
|
+
const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
|
|
1462
|
+
const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
|
|
1463
|
+
if (sourceMatch) newImports.push({
|
|
1464
|
+
key: imp.key + "#supplemental",
|
|
1465
|
+
text: `${prefix} { ${newNames.join(", ")} } from ${sourceMatch[1]};`
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
preserved++;
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const braceMatch = imp.text.match(/\{([^}]+)\}/);
|
|
1473
|
+
if (braceMatch) {
|
|
1474
|
+
const names = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean);
|
|
1475
|
+
const isAlreadyDefined = (n) => existingImportedNames.has(n) || existingKeys.has(n);
|
|
1476
|
+
if (names.length > 0 && names.every(isAlreadyDefined)) {
|
|
1477
|
+
preserved++;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (names.some(isAlreadyDefined)) {
|
|
1481
|
+
const newNames = names.filter((n) => !isAlreadyDefined(n));
|
|
1482
|
+
if (newNames.length === 0) {
|
|
1483
|
+
preserved++;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
|
|
1487
|
+
const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
|
|
1488
|
+
if (sourceMatch) {
|
|
1489
|
+
const newText = `${prefix} { ${newNames.join(", ")} } from ${sourceMatch[1]};`;
|
|
1490
|
+
newImports.push({
|
|
1491
|
+
key: imp.key,
|
|
1492
|
+
text: newText
|
|
1493
|
+
});
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
newImports.push(imp);
|
|
1499
|
+
}
|
|
1500
|
+
for (const stmt of generatedStatements.statements) {
|
|
1501
|
+
if (stmt.text.trim() === headerLine) continue;
|
|
1502
|
+
if (stmt.kind === "reexport") {
|
|
1503
|
+
const normalizedText = adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim();
|
|
1504
|
+
if (existingReexports.has(normalizedText) || stmt.key !== null && existingKeys.has(stmt.key)) {
|
|
1505
|
+
preserved++;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (stmt.key && (existingKeys.has(stmt.key) || existingImportedNames.has(stmt.key))) {
|
|
1510
|
+
preserved++;
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (!stmt.key) {
|
|
1514
|
+
if (existingUnnamedTexts.has(stmt.text.trim())) {
|
|
1515
|
+
preserved++;
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
toAppend.push(stmt.text);
|
|
1520
|
+
}
|
|
1521
|
+
if (newImports.length === 0 && toAppend.length === 0) {
|
|
1522
|
+
if (!adapter.extractMembers) {}
|
|
1523
|
+
}
|
|
1524
|
+
let result = existingContent;
|
|
1525
|
+
if (toAppend.length > 0) result = result.trimEnd() + "\n\n" + toAppend.join("\n\n") + "\n";
|
|
1526
|
+
let deepAdded = 0;
|
|
1527
|
+
const insertions = [];
|
|
1528
|
+
if (adapter.extractMembers) {
|
|
1529
|
+
const parser = await getParser(language);
|
|
1530
|
+
const resultTree = safeParse(parser, result);
|
|
1531
|
+
const generatedTree = safeParse(parser, generatedContent);
|
|
1532
|
+
const resultSymbols = adapter.extractMembers(resultTree, result);
|
|
1533
|
+
const generatedSymbols = adapter.extractMembers(generatedTree, generatedContent);
|
|
1534
|
+
const existingTree = safeParse(parser, existingContent);
|
|
1535
|
+
const deepIgnoredSymbols = buildIgnoredSymbolNames(adapter.extractDocstrings(existingTree, existingContent), findIgnoredRegions(existingContent));
|
|
1536
|
+
for (const [symbolName, genSymbol] of generatedSymbols) {
|
|
1537
|
+
if (deepIgnoredSymbols.has(symbolName)) continue;
|
|
1538
|
+
const existSymbol = resultSymbols.get(symbolName);
|
|
1539
|
+
if (!existSymbol) continue;
|
|
1540
|
+
const existingMemberKeys = new Set(existSymbol.members.map((m) => m.key));
|
|
1541
|
+
const newMembers = genSymbol.members.filter((m) => !existingMemberKeys.has(m.key));
|
|
1542
|
+
if (newMembers.length > 0) {
|
|
1543
|
+
if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
|
|
1544
|
+
const indent = existSymbol.memberIndent ?? " ";
|
|
1545
|
+
const insertText = newMembers.map((m) => indent + m.text).join("\n");
|
|
1546
|
+
insertions.push({
|
|
1547
|
+
line: existSymbol.bodyEndLine,
|
|
1548
|
+
text: insertText
|
|
1549
|
+
});
|
|
1550
|
+
deepAdded += newMembers.length;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (insertions.length > 0) {
|
|
1554
|
+
insertions.sort((a, b) => b.line - a.line);
|
|
1555
|
+
const resultLines = result.split("\n");
|
|
1556
|
+
for (const ins of insertions) resultLines.splice(ins.line, 0, ins.text);
|
|
1557
|
+
result = resultLines.join("\n");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (newImports.length > 0 && (toAppend.length > 0 || deepAdded > 0)) {
|
|
1561
|
+
const addedParts = [...toAppend];
|
|
1562
|
+
if (insertions) for (const ins of insertions) addedParts.push(ins.text);
|
|
1563
|
+
const addedCodeText = addedParts.join("\n");
|
|
1564
|
+
const filteredImports = [];
|
|
1565
|
+
for (const imp of newImports) {
|
|
1566
|
+
const braceMatch = imp.text.match(/\{([^}]+)\}/);
|
|
1567
|
+
if (!braceMatch) {
|
|
1568
|
+
filteredImports.push(imp);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
const names = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean);
|
|
1572
|
+
const usedNames = names.filter((name) => addedCodeText.includes(name));
|
|
1573
|
+
if (usedNames.length === 0) continue;
|
|
1574
|
+
if (usedNames.length === names.length) filteredImports.push(imp);
|
|
1575
|
+
else {
|
|
1576
|
+
const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
|
|
1577
|
+
const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
|
|
1578
|
+
if (sourceMatch) filteredImports.push({
|
|
1579
|
+
key: imp.key,
|
|
1580
|
+
text: `${prefix} { ${usedNames.join(", ")} } from ${sourceMatch[1]};`
|
|
1581
|
+
});
|
|
1582
|
+
else filteredImports.push(imp);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (filteredImports.length > 0) {
|
|
1586
|
+
const renderedImports = adapter.renderImports ? adapter.renderImports(filteredImports) : filteredImports.map((entry) => entry.text);
|
|
1587
|
+
const lines = result.split("\n");
|
|
1588
|
+
const insertIdx = lastImportEndIndex + 1;
|
|
1589
|
+
lines.splice(insertIdx, 0, ...renderedImports);
|
|
1590
|
+
result = lines.join("\n");
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
let docstringUpdates = 0;
|
|
1594
|
+
{
|
|
1595
|
+
const parser = await getParser(language);
|
|
1596
|
+
const resultTree = safeParse(parser, result);
|
|
1597
|
+
const generatedTree = safeParse(parser, generatedContent);
|
|
1598
|
+
const resultDocs = adapter.extractDocstrings(resultTree, result);
|
|
1599
|
+
const generatedDocs = adapter.extractDocstrings(generatedTree, generatedContent);
|
|
1600
|
+
const docIgnoredSymbols = buildIgnoredSymbolNames(resultDocs, findIgnoredRegions(result));
|
|
1601
|
+
const edits = [];
|
|
1602
|
+
for (const [symbolName, genInfo] of generatedDocs) {
|
|
1603
|
+
const existInfo = resultDocs.get(symbolName);
|
|
1604
|
+
if (!existInfo) continue;
|
|
1605
|
+
if (docIgnoredSymbols.has(symbolName)) continue;
|
|
1606
|
+
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
|
|
1607
|
+
if (genDoc) if (existInfo.docstring) {
|
|
1608
|
+
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
1609
|
+
edits.push({
|
|
1610
|
+
start: existInfo.docstring.startIndex,
|
|
1611
|
+
end: existInfo.docstring.endIndex,
|
|
1612
|
+
newText: genDoc.text
|
|
1613
|
+
});
|
|
1614
|
+
docstringUpdates++;
|
|
1615
|
+
}
|
|
1616
|
+
} else {
|
|
1617
|
+
const lineStart = existInfo.declStartIndex - existInfo.declColumn;
|
|
1618
|
+
const indent = " ".repeat(existInfo.declColumn);
|
|
1619
|
+
edits.push({
|
|
1620
|
+
start: lineStart,
|
|
1621
|
+
end: lineStart,
|
|
1622
|
+
newText: indent + genDoc.text + "\n"
|
|
1623
|
+
});
|
|
1624
|
+
docstringUpdates++;
|
|
1625
|
+
}
|
|
1626
|
+
const matchedExistMembers = /* @__PURE__ */ new Set();
|
|
1627
|
+
for (const [memberName, genMember] of genInfo.members) {
|
|
1628
|
+
const existMember = existInfo.members.get(memberName);
|
|
1629
|
+
if (!existMember || !genMember.docstring) continue;
|
|
1630
|
+
matchedExistMembers.add(memberName);
|
|
1631
|
+
if (existMember.docstring) {
|
|
1632
|
+
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
1633
|
+
edits.push({
|
|
1634
|
+
start: existMember.docstring.startIndex,
|
|
1635
|
+
end: existMember.docstring.endIndex,
|
|
1636
|
+
newText: genMember.docstring.text
|
|
1637
|
+
});
|
|
1638
|
+
docstringUpdates++;
|
|
1639
|
+
}
|
|
1640
|
+
} else {
|
|
1641
|
+
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
1642
|
+
const indent = " ".repeat(existMember.declColumn);
|
|
1643
|
+
edits.push({
|
|
1644
|
+
start: lineStart,
|
|
1645
|
+
end: lineStart,
|
|
1646
|
+
newText: indent + genMember.docstring.text + "\n"
|
|
1647
|
+
});
|
|
1648
|
+
docstringUpdates++;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
for (const [_genName, genMember] of genInfo.members) {
|
|
1652
|
+
if (!genMember.docstring || !genMember.urlFingerprint) continue;
|
|
1653
|
+
for (const [existName, existMember] of existInfo.members) {
|
|
1654
|
+
if (matchedExistMembers.has(existName)) continue;
|
|
1655
|
+
if (!existMember.urlFingerprint || existMember.urlFingerprint !== genMember.urlFingerprint) continue;
|
|
1656
|
+
if (existMember.docstring) {
|
|
1657
|
+
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
1658
|
+
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
1659
|
+
edits.push({
|
|
1660
|
+
start: existMember.docstring.startIndex,
|
|
1661
|
+
end: existMember.docstring.endIndex,
|
|
1662
|
+
newText: genMember.docstring.text
|
|
1663
|
+
});
|
|
1664
|
+
docstringUpdates++;
|
|
1665
|
+
}
|
|
1666
|
+
} else {
|
|
1667
|
+
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
1668
|
+
const indent = " ".repeat(existMember.declColumn);
|
|
1669
|
+
edits.push({
|
|
1670
|
+
start: lineStart,
|
|
1671
|
+
end: lineStart,
|
|
1672
|
+
newText: indent + genMember.docstring.text + "\n"
|
|
1673
|
+
});
|
|
1674
|
+
docstringUpdates++;
|
|
1675
|
+
}
|
|
1676
|
+
matchedExistMembers.add(existName);
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (edits.length > 0) {
|
|
1682
|
+
edits.sort((a, b) => b.start - a.start);
|
|
1683
|
+
for (const edit of edits) result = result.slice(0, edit.start) + edit.newText + result.slice(edit.end);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
const totalAdded = (toAppend.length > 0 || deepAdded > 0 ? adapter.renderImports ? adapter.renderImports(newImports).length : newImports.length : 0) + toAppend.length + deepAdded;
|
|
1687
|
+
if (totalAdded === 0 && docstringUpdates === 0) return {
|
|
1688
|
+
content: existingContent,
|
|
1689
|
+
added: 0,
|
|
1690
|
+
preserved,
|
|
1691
|
+
changed: false
|
|
1692
|
+
};
|
|
1693
|
+
return {
|
|
1694
|
+
content: result,
|
|
1695
|
+
added: totalAdded,
|
|
1696
|
+
preserved,
|
|
1697
|
+
changed: true
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1701
|
+
//#region src/engine/json-merge.ts
|
|
1702
|
+
/**
|
|
1703
|
+
* Deep merge two parsed JSON values.
|
|
1704
|
+
*
|
|
1705
|
+
* - Objects: recursively merge, preserving existing keys not in generated
|
|
1706
|
+
* - Arrays: generated replaces existing entirely (generated owns array contents)
|
|
1707
|
+
* - Primitives: generated wins
|
|
1708
|
+
*/
|
|
1709
|
+
function deepMergeJson(existing, generated) {
|
|
1710
|
+
if (typeof existing === "object" && existing !== null && !Array.isArray(existing) && typeof generated === "object" && generated !== null && !Array.isArray(generated)) {
|
|
1711
|
+
const merged = { ...existing };
|
|
1712
|
+
for (const [key, value] of Object.entries(generated)) if (key in merged) merged[key] = deepMergeJson(merged[key], value);
|
|
1713
|
+
else merged[key] = value;
|
|
1714
|
+
return merged;
|
|
1715
|
+
}
|
|
1716
|
+
return generated;
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region src/engine/writer.ts
|
|
1720
|
+
/**
|
|
1721
|
+
* Write generated files to disk with AST-level merging.
|
|
1722
|
+
*
|
|
1723
|
+
* When a file already exists:
|
|
1724
|
+
* - skipIfExists → leave it alone entirely
|
|
1725
|
+
* - AST merge available → parse both, append only new symbols
|
|
1726
|
+
* - No grammar → skip (never clobber hand-written code)
|
|
1727
|
+
*
|
|
1728
|
+
* New files are always written in full.
|
|
1729
|
+
*/
|
|
1730
|
+
async function writeFiles(files, outputDir, options) {
|
|
1731
|
+
const result = {
|
|
1732
|
+
written: [],
|
|
1733
|
+
merged: [],
|
|
1734
|
+
skipped: [],
|
|
1735
|
+
identical: [],
|
|
1736
|
+
ignored: []
|
|
1737
|
+
};
|
|
1738
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
1739
|
+
const language = options?.language;
|
|
1740
|
+
const header = options?.header ?? "";
|
|
1741
|
+
for (const file of sorted) {
|
|
1742
|
+
const fullPath = path$1.join(outputDir, file.path);
|
|
1743
|
+
let existingContent = null;
|
|
1744
|
+
try {
|
|
1745
|
+
existingContent = await fs.readFile(fullPath, "utf-8");
|
|
1746
|
+
} catch {}
|
|
1747
|
+
if (existingContent === null) {
|
|
1748
|
+
await fs.mkdir(path$1.dirname(fullPath), { recursive: true });
|
|
1749
|
+
await fs.writeFile(fullPath, file.content, "utf-8");
|
|
1750
|
+
result.written.push(file.path);
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
if (file.skipIfExists) {
|
|
1754
|
+
if (header && !existingContent.startsWith(header)) await fs.writeFile(fullPath, header + "\n\n" + existingContent, "utf-8");
|
|
1755
|
+
result.skipped.push(file.path);
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (existingContent.includes("@oagen-ignore-file")) {
|
|
1759
|
+
result.ignored.push(file.path);
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const adapter = language ? getMergeAdapter(language) : void 0;
|
|
1763
|
+
if (adapter?.testFilePatterns ? adapter.testFilePatterns.some((p) => p.test(file.path)) : isDefaultTestFile(file.path)) {
|
|
1764
|
+
result.skipped.push(file.path);
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (existingContent === file.content) {
|
|
1768
|
+
result.identical.push(file.path);
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (file.overwriteExisting) {
|
|
1772
|
+
await fs.writeFile(fullPath, file.content, "utf-8");
|
|
1773
|
+
result.written.push(file.path);
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (file.path.endsWith(".json")) {
|
|
1777
|
+
try {
|
|
1778
|
+
const merged = deepMergeJson(JSON.parse(existingContent), JSON.parse(file.content));
|
|
1779
|
+
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
1780
|
+
if (mergedContent === existingContent) result.identical.push(file.path);
|
|
1781
|
+
else {
|
|
1782
|
+
await fs.writeFile(fullPath, mergedContent, "utf-8");
|
|
1783
|
+
result.merged.push(file.path);
|
|
1784
|
+
}
|
|
1785
|
+
} catch {
|
|
1786
|
+
await fs.writeFile(fullPath, file.content, "utf-8");
|
|
1787
|
+
result.written.push(file.path);
|
|
1788
|
+
}
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
if (language && hasGrammar(language)) try {
|
|
1792
|
+
const mergeResult = await mergeIntoExisting(existingContent, file.content, language, header);
|
|
1793
|
+
let finalContent = mergeResult.content;
|
|
1794
|
+
if (header && !finalContent.startsWith(header)) finalContent = header + "\n\n" + finalContent;
|
|
1795
|
+
if (!mergeResult.changed && finalContent === existingContent) {
|
|
1796
|
+
result.identical.push(file.path);
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
await fs.writeFile(fullPath, finalContent, "utf-8");
|
|
1800
|
+
result.merged.push(file.path);
|
|
1801
|
+
continue;
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
console.warn(`[oagen] AST merge failed for ${file.path}, falling back to overwrite.${err instanceof Error ? ` ${err.message}` : ""}`);
|
|
1804
|
+
await fs.writeFile(fullPath, file.content, "utf-8");
|
|
1805
|
+
result.written.push(file.path);
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
result.skipped.push(file.path);
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
}
|
|
1812
|
+
/** Comprehensive fallback when no adapter is available. */
|
|
1813
|
+
function isDefaultTestFile(filePath) {
|
|
1814
|
+
return /\.(spec|test)\.[jt]sx?$/.test(filePath) || filePath.endsWith("_test.go") || filePath.endsWith("_test.rb") || filePath.endsWith("_spec.rb") || /(?:^|\/)test_.*\.py$/.test(filePath) || filePath.endsWith("_test.py") || filePath.endsWith("Test.php") || filePath.endsWith("_test.rs") || /(?:^|\/)tests\/.*\.rs$/.test(filePath);
|
|
1815
|
+
}
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region src/engine/generate-files.ts
|
|
1818
|
+
function buildEmitterContext(spec, options) {
|
|
1819
|
+
return {
|
|
1820
|
+
namespace: toSnakeCase(options.namespace),
|
|
1821
|
+
namespacePascal: options.namespace,
|
|
1822
|
+
spec,
|
|
1823
|
+
outputDir: options.outputDir,
|
|
1824
|
+
apiSurface: options.apiSurface,
|
|
1825
|
+
overlayLookup: options.overlayLookup
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Collect model and enum names transitively referenced by service operations.
|
|
1830
|
+
* Walks operation parameters, request bodies, responses, and pagination item
|
|
1831
|
+
* types, then chases model field references until the set stabilizes.
|
|
1832
|
+
*/
|
|
1833
|
+
function collectReferencedNames(services, models) {
|
|
1834
|
+
const referencedModels = /* @__PURE__ */ new Set();
|
|
1835
|
+
const referencedEnums = /* @__PURE__ */ new Set();
|
|
1836
|
+
const collectFromTypeRef = (ref) => {
|
|
1837
|
+
walkTypeRef(ref, {
|
|
1838
|
+
model: (r) => referencedModels.add(r.name),
|
|
1839
|
+
enum: (r) => referencedEnums.add(r.name)
|
|
1840
|
+
});
|
|
1841
|
+
};
|
|
1842
|
+
for (const service of services) for (const op of service.operations) {
|
|
1843
|
+
for (const p of [
|
|
1844
|
+
...op.pathParams,
|
|
1845
|
+
...op.queryParams,
|
|
1846
|
+
...op.headerParams,
|
|
1847
|
+
...op.cookieParams ?? []
|
|
1848
|
+
]) collectFromTypeRef(p.type);
|
|
1849
|
+
if (op.requestBody) collectFromTypeRef(op.requestBody);
|
|
1850
|
+
collectFromTypeRef(op.response);
|
|
1851
|
+
if (op.pagination) collectFromTypeRef(op.pagination.itemType);
|
|
1852
|
+
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
1853
|
+
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
1854
|
+
}
|
|
1855
|
+
const modelsByName = new Map(models.map((m) => [m.name, m]));
|
|
1856
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1857
|
+
const queue = [...referencedModels];
|
|
1858
|
+
while (queue.length > 0) {
|
|
1859
|
+
const name = queue.pop();
|
|
1860
|
+
if (visited.has(name)) continue;
|
|
1861
|
+
visited.add(name);
|
|
1862
|
+
const model = modelsByName.get(name);
|
|
1863
|
+
if (!model) continue;
|
|
1864
|
+
for (const field of model.fields) {
|
|
1865
|
+
collectFromTypeRef(field.type);
|
|
1866
|
+
for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return {
|
|
1870
|
+
models: referencedModels,
|
|
1871
|
+
enums: referencedEnums
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
/** Collect all generated files from an emitter (no headers, no path prefixes). */
|
|
1875
|
+
function generateAllFiles(spec, emitter, ctx) {
|
|
1876
|
+
const referenced = collectReferencedNames(spec.services, spec.models);
|
|
1877
|
+
const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
|
|
1878
|
+
const reachableEnums = spec.enums.filter((e) => referenced.enums.has(e.name));
|
|
1879
|
+
const reachableSpec = {
|
|
1880
|
+
...spec,
|
|
1881
|
+
models: reachableModels,
|
|
1882
|
+
enums: reachableEnums
|
|
1883
|
+
};
|
|
1884
|
+
return [
|
|
1885
|
+
...emitter.generateModels(reachableModels, ctx),
|
|
1886
|
+
...emitter.generateEnums(reachableEnums, ctx),
|
|
1887
|
+
...emitter.generateResources(spec.services, ctx),
|
|
1888
|
+
...emitter.generateClient(spec, ctx),
|
|
1889
|
+
...emitter.generateErrors(ctx),
|
|
1890
|
+
...emitter.generateConfig(ctx),
|
|
1891
|
+
...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
|
|
1892
|
+
...emitter.generateTests(reachableSpec, ctx),
|
|
1893
|
+
...emitter.generateManifest?.(spec, ctx) ?? []
|
|
1894
|
+
];
|
|
1895
|
+
}
|
|
1896
|
+
/** Apply file header to generated files, respecting headerPlacement and JSON files. */
|
|
1897
|
+
function applyFileHeaders(files, header) {
|
|
1898
|
+
return files.map((f) => ({
|
|
1899
|
+
...f,
|
|
1900
|
+
content: !header || f.path.endsWith(".json") || f.headerPlacement === "skip" ? f.content : header + "\n\n" + f.content,
|
|
1901
|
+
skipIfExists: f.skipIfExists ?? false
|
|
1902
|
+
}));
|
|
1903
|
+
}
|
|
1904
|
+
function generateFiles(spec, emitter, options) {
|
|
1905
|
+
const ctx = buildEmitterContext(spec, options);
|
|
1906
|
+
const files = generateAllFiles(spec, emitter, ctx);
|
|
1907
|
+
const header = emitter.fileHeader();
|
|
1908
|
+
return {
|
|
1909
|
+
files: applyFileHeaders(files, header),
|
|
1910
|
+
ctx,
|
|
1911
|
+
header
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/engine/integrate.ts
|
|
1916
|
+
/**
|
|
1917
|
+
* Resolve a relative import path against a source file path to get the
|
|
1918
|
+
* target file path. Handles implicit .ts extensions and /index.ts.
|
|
1919
|
+
*/
|
|
1920
|
+
function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
|
|
1921
|
+
const cleaned = importModulePath.replace(/^['"]|['"]$/g, "");
|
|
1922
|
+
if (!cleaned.startsWith(".")) return void 0;
|
|
1923
|
+
const dir = path.dirname(fromFilePath);
|
|
1924
|
+
const resolved = path.normalize(path.join(dir, cleaned));
|
|
1925
|
+
for (const candidate of [
|
|
1926
|
+
resolved,
|
|
1927
|
+
`${resolved}.ts`,
|
|
1928
|
+
`${resolved}/index.ts`
|
|
1929
|
+
]) if (knownPaths.has(candidate)) return candidate;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Determine whether a generated file is a "root" — a file that should always
|
|
1933
|
+
* be integrated into the target. Roots are entry points from which we trace
|
|
1934
|
+
* the import graph. Everything else must be reachable from a root.
|
|
1935
|
+
*
|
|
1936
|
+
* Roots: resource classes, client, config, errors, enums, barrel index files.
|
|
1937
|
+
* Non-roots: serializers, fixtures, standalone interfaces, test files.
|
|
1938
|
+
*/
|
|
1939
|
+
function isRootFile(filePath) {
|
|
1940
|
+
if (filePath.includes("/fixtures/")) return false;
|
|
1941
|
+
if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return false;
|
|
1942
|
+
if (filePath.includes("/serializers/")) return false;
|
|
1943
|
+
if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Build a file-level dependency graph from generated files and return only
|
|
1948
|
+
* files reachable from root entry points. Uses tree-sitter to parse imports.
|
|
1949
|
+
*/
|
|
1950
|
+
async function treeShakeFiles(files, language) {
|
|
1951
|
+
if (!hasGrammar(language)) return files;
|
|
1952
|
+
const knownPaths = new Set(files.map((f) => f.path));
|
|
1953
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1954
|
+
for (const file of files) {
|
|
1955
|
+
if (!file.path.endsWith(".ts") && !file.path.endsWith(".js")) {
|
|
1956
|
+
deps.set(file.path, /* @__PURE__ */ new Set());
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const parsed = await extractStatements(file.content, language);
|
|
1960
|
+
const fileDeps = /* @__PURE__ */ new Set();
|
|
1961
|
+
for (const imp of parsed.imports) {
|
|
1962
|
+
const resolved = resolveImportPath(file.path, imp.key, knownPaths);
|
|
1963
|
+
if (resolved) fileDeps.add(resolved);
|
|
1964
|
+
}
|
|
1965
|
+
for (const stmt of parsed.statements) if (stmt.kind === "reexport") {
|
|
1966
|
+
const match = stmt.text.match(/from\s+['"]([^'"]+)['"]/);
|
|
1967
|
+
if (match) {
|
|
1968
|
+
const resolved = resolveImportPath(file.path, match[1], knownPaths);
|
|
1969
|
+
if (resolved) fileDeps.add(resolved);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
deps.set(file.path, fileDeps);
|
|
1973
|
+
}
|
|
1974
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1975
|
+
const queue = [];
|
|
1976
|
+
for (const file of files) if (isRootFile(file.path)) {
|
|
1977
|
+
reachable.add(file.path);
|
|
1978
|
+
queue.push(file.path);
|
|
1979
|
+
}
|
|
1980
|
+
while (queue.length > 0) {
|
|
1981
|
+
const current = queue.pop();
|
|
1982
|
+
const fileDeps = deps.get(current);
|
|
1983
|
+
if (!fileDeps) continue;
|
|
1984
|
+
for (const dep of fileDeps) if (!reachable.has(dep)) {
|
|
1985
|
+
reachable.add(dep);
|
|
1986
|
+
queue.push(dep);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return files.filter((f) => reachable.has(f.path));
|
|
1990
|
+
}
|
|
1991
|
+
function mapFilesForTargetIntegration(files, language) {
|
|
1992
|
+
const langPrefix = `${language}/`;
|
|
1993
|
+
return files.filter((f) => f.integrateTarget !== false).map((f) => {
|
|
1994
|
+
const stripped = f.path.startsWith(langPrefix) ? f.path.replace(langPrefix, "") : f.path;
|
|
1995
|
+
return {
|
|
1996
|
+
...f,
|
|
1997
|
+
skipIfExists: false,
|
|
1998
|
+
path: stripped
|
|
1999
|
+
};
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
async function integrateGeneratedFiles(opts) {
|
|
2003
|
+
return writeFiles(await treeShakeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.language), opts.targetDir, {
|
|
2004
|
+
language: opts.language,
|
|
2005
|
+
header: opts.header
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/engine/formatter.ts
|
|
2010
|
+
/**
|
|
2011
|
+
* Post-integration formatter.
|
|
2012
|
+
*
|
|
2013
|
+
* After generating and merging files into a target directory, runs the
|
|
2014
|
+
* emitter-provided format command on all written/merged files so that
|
|
2015
|
+
* generated code matches the target project's style conventions.
|
|
2016
|
+
*/
|
|
2017
|
+
const execFileAsync = promisify(execFile);
|
|
2018
|
+
/**
|
|
2019
|
+
* Run the emitter's format command on the given target files.
|
|
2020
|
+
* Silently skips if the emitter has no formatCommand or returns null.
|
|
2021
|
+
*/
|
|
2022
|
+
async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
2023
|
+
if (filePaths.length === 0) return;
|
|
2024
|
+
if (!emitter.formatCommand) return;
|
|
2025
|
+
const formatCmd = emitter.formatCommand(targetDir);
|
|
2026
|
+
if (!formatCmd) return;
|
|
2027
|
+
const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
|
|
2028
|
+
const batchSize = formatCmd.batchSize ?? 100;
|
|
2029
|
+
for (let i = 0; i < absolutePaths.length; i += batchSize) {
|
|
2030
|
+
const batch = absolutePaths.slice(i, i + batchSize);
|
|
2031
|
+
try {
|
|
2032
|
+
await execFileAsync(formatCmd.cmd, [...formatCmd.args, ...batch], {
|
|
2033
|
+
cwd: targetDir,
|
|
2034
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2035
|
+
timeout: 12e4
|
|
2036
|
+
});
|
|
2037
|
+
} catch {}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
//#endregion
|
|
2041
|
+
//#region src/engine/orchestrator.ts
|
|
2042
|
+
async function generate(spec, emitter, options) {
|
|
2043
|
+
const { files: withHeaders, header } = generateFiles(spec, emitter, options);
|
|
2044
|
+
if (options.dryRun) {
|
|
2045
|
+
if (options.target) {
|
|
2046
|
+
console.log(`\nTarget integration (${options.target}):`);
|
|
2047
|
+
for (const f of withHeaders) console.log(` ${f.path}`);
|
|
2048
|
+
}
|
|
2049
|
+
return withHeaders;
|
|
2050
|
+
}
|
|
2051
|
+
const writeResult = await writeFiles(withHeaders, options.outputDir, {
|
|
2052
|
+
language: emitter.language,
|
|
2053
|
+
header
|
|
2054
|
+
});
|
|
2055
|
+
if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
|
|
2056
|
+
if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
|
|
2057
|
+
if (options.target) {
|
|
2058
|
+
const targetResult = await integrateGeneratedFiles({
|
|
2059
|
+
files: withHeaders,
|
|
2060
|
+
language: emitter.language,
|
|
2061
|
+
targetDir: options.target,
|
|
2062
|
+
header
|
|
2063
|
+
});
|
|
2064
|
+
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
2065
|
+
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
2066
|
+
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
2067
|
+
const allTargetFiles = [...targetResult.written, ...targetResult.merged];
|
|
2068
|
+
if (allTargetFiles.length > 0) await formatTargetFiles(emitter, options.target, allTargetFiles);
|
|
2069
|
+
}
|
|
2070
|
+
return withHeaders;
|
|
2071
|
+
}
|
|
2072
|
+
//#endregion
|
|
2073
|
+
//#region src/engine/registry.ts
|
|
2074
|
+
const emitters = /* @__PURE__ */ new Map();
|
|
2075
|
+
function registerEmitter(emitter) {
|
|
2076
|
+
emitters.set(emitter.language, emitter);
|
|
2077
|
+
}
|
|
2078
|
+
function getEmitter(language) {
|
|
2079
|
+
const emitter = emitters.get(language);
|
|
2080
|
+
if (!emitter) throw new RegistryError(`Unknown language: ${language}. Available: ${[...emitters.keys()].join(", ") || "(none)"}`, `Register an emitter for "${language}" via registerEmitter() or check your oagen.config.ts plugin configuration.`);
|
|
2081
|
+
return emitter;
|
|
2082
|
+
}
|
|
2083
|
+
//#endregion
|
|
2084
|
+
//#region src/differ/classify.ts
|
|
2085
|
+
function classifyFieldChange(kind, fieldName, isRequired) {
|
|
2086
|
+
switch (kind) {
|
|
2087
|
+
case "field-added": return {
|
|
2088
|
+
kind,
|
|
2089
|
+
fieldName,
|
|
2090
|
+
classification: isRequired ? "breaking" : "additive"
|
|
2091
|
+
};
|
|
2092
|
+
case "field-removed": return {
|
|
2093
|
+
kind,
|
|
2094
|
+
fieldName,
|
|
2095
|
+
classification: "breaking"
|
|
2096
|
+
};
|
|
2097
|
+
case "field-type-changed": return {
|
|
2098
|
+
kind,
|
|
2099
|
+
fieldName,
|
|
2100
|
+
classification: "breaking"
|
|
2101
|
+
};
|
|
2102
|
+
case "field-required-changed": return {
|
|
2103
|
+
kind,
|
|
2104
|
+
fieldName,
|
|
2105
|
+
classification: isRequired ? "breaking" : "additive",
|
|
2106
|
+
details: isRequired ? "optional → required" : "required → optional"
|
|
2107
|
+
};
|
|
2108
|
+
case "field-access-changed": return {
|
|
2109
|
+
kind,
|
|
2110
|
+
fieldName,
|
|
2111
|
+
classification: "breaking"
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function classifyParamChange(kind, paramName, isRequired) {
|
|
2116
|
+
switch (kind) {
|
|
2117
|
+
case "param-added": return {
|
|
2118
|
+
kind,
|
|
2119
|
+
paramName,
|
|
2120
|
+
classification: isRequired ? "breaking" : "additive"
|
|
2121
|
+
};
|
|
2122
|
+
case "param-removed": return {
|
|
2123
|
+
kind,
|
|
2124
|
+
paramName,
|
|
2125
|
+
classification: "breaking"
|
|
2126
|
+
};
|
|
2127
|
+
case "param-type-changed": return {
|
|
2128
|
+
kind,
|
|
2129
|
+
paramName,
|
|
2130
|
+
classification: "breaking"
|
|
2131
|
+
};
|
|
2132
|
+
case "param-required-changed": return {
|
|
2133
|
+
kind,
|
|
2134
|
+
paramName,
|
|
2135
|
+
classification: isRequired ? "breaking" : "additive"
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
//#endregion
|
|
2140
|
+
//#region src/differ/models.ts
|
|
2141
|
+
function diffModels(oldModels, newModels) {
|
|
2142
|
+
const changes = [];
|
|
2143
|
+
const oldByName = new Map(oldModels.map((m) => [m.name, m]));
|
|
2144
|
+
const newByName = new Map(newModels.map((m) => [m.name, m]));
|
|
2145
|
+
for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
|
|
2146
|
+
kind: "model-added",
|
|
2147
|
+
name,
|
|
2148
|
+
classification: "additive"
|
|
2149
|
+
});
|
|
2150
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
|
|
2151
|
+
kind: "model-removed",
|
|
2152
|
+
name,
|
|
2153
|
+
classification: "breaking"
|
|
2154
|
+
});
|
|
2155
|
+
for (const [name, newModel] of newByName) {
|
|
2156
|
+
const oldModel = oldByName.get(name);
|
|
2157
|
+
if (!oldModel) continue;
|
|
2158
|
+
const fieldChanges = diffFields(oldModel, newModel);
|
|
2159
|
+
if (fieldChanges.length > 0) {
|
|
2160
|
+
const hasBreaking = fieldChanges.some((fc) => fc.classification === "breaking");
|
|
2161
|
+
changes.push({
|
|
2162
|
+
kind: "model-modified",
|
|
2163
|
+
name,
|
|
2164
|
+
fieldChanges,
|
|
2165
|
+
classification: hasBreaking ? "breaking" : "additive"
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return changes;
|
|
2170
|
+
}
|
|
2171
|
+
function diffFields(oldModel, newModel) {
|
|
2172
|
+
const changes = [];
|
|
2173
|
+
const oldByName = new Map(oldModel.fields.map((f) => [f.name, f]));
|
|
2174
|
+
const newByName = new Map(newModel.fields.map((f) => [f.name, f]));
|
|
2175
|
+
for (const [name, field] of newByName) if (!oldByName.has(name)) changes.push(classifyFieldChange("field-added", name, field.required));
|
|
2176
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push(classifyFieldChange("field-removed", name));
|
|
2177
|
+
for (const [name, newField] of newByName) {
|
|
2178
|
+
const oldField = oldByName.get(name);
|
|
2179
|
+
if (!oldField) continue;
|
|
2180
|
+
if (!typeRefsEqual(oldField.type, newField.type)) changes.push({
|
|
2181
|
+
kind: "field-type-changed",
|
|
2182
|
+
fieldName: name,
|
|
2183
|
+
classification: "breaking",
|
|
2184
|
+
details: `type changed`
|
|
2185
|
+
});
|
|
2186
|
+
if (oldField.required !== newField.required) changes.push(classifyFieldChange("field-required-changed", name, newField.required));
|
|
2187
|
+
if (!!oldField.readOnly !== !!newField.readOnly || !!oldField.writeOnly !== !!newField.writeOnly) changes.push(classifyFieldChange("field-access-changed", name));
|
|
2188
|
+
}
|
|
2189
|
+
return changes;
|
|
2190
|
+
}
|
|
2191
|
+
function typeRefsEqual(a, b) {
|
|
2192
|
+
if (a.kind !== b.kind) return false;
|
|
2193
|
+
switch (a.kind) {
|
|
2194
|
+
case "primitive": return b.kind === "primitive" && a.type === b.type && (a.format ?? "") === (b.format ?? "");
|
|
2195
|
+
case "array": return b.kind === "array" && typeRefsEqual(a.items, b.items);
|
|
2196
|
+
case "model": return b.kind === "model" && a.name === b.name;
|
|
2197
|
+
case "enum": return b.kind === "enum" && a.name === b.name;
|
|
2198
|
+
case "literal": return b.kind === "literal" && a.value === b.value;
|
|
2199
|
+
case "nullable": return b.kind === "nullable" && typeRefsEqual(a.inner, b.inner);
|
|
2200
|
+
case "union": {
|
|
2201
|
+
if (b.kind !== "union") return false;
|
|
2202
|
+
if (a.variants.length !== b.variants.length) return false;
|
|
2203
|
+
if (!a.variants.every((v, i) => typeRefsEqual(v, b.variants[i]))) return false;
|
|
2204
|
+
if (!a.discriminator && !b.discriminator) return true;
|
|
2205
|
+
if (!a.discriminator || !b.discriminator) return false;
|
|
2206
|
+
if (a.discriminator.property !== b.discriminator.property) return false;
|
|
2207
|
+
const aKeys = Object.keys(a.discriminator.mapping).sort();
|
|
2208
|
+
const bKeys = Object.keys(b.discriminator.mapping).sort();
|
|
2209
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
2210
|
+
return aKeys.every((k, i) => k === bKeys[i] && a.discriminator.mapping[k] === b.discriminator.mapping[k]);
|
|
2211
|
+
}
|
|
2212
|
+
case "map": return b.kind === "map" && typeRefsEqual(a.valueType, b.valueType);
|
|
2213
|
+
}
|
|
2214
|
+
return assertNever(a);
|
|
2215
|
+
}
|
|
2216
|
+
//#endregion
|
|
2217
|
+
//#region src/differ/operations.ts
|
|
2218
|
+
function diffOperations(serviceName, oldOps, newOps) {
|
|
2219
|
+
const changes = [];
|
|
2220
|
+
const oldByName = new Map(oldOps.map((o) => [o.name, o]));
|
|
2221
|
+
const newByName = new Map(newOps.map((o) => [o.name, o]));
|
|
2222
|
+
for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
|
|
2223
|
+
kind: "operation-added",
|
|
2224
|
+
serviceName,
|
|
2225
|
+
operationName: name,
|
|
2226
|
+
classification: "additive"
|
|
2227
|
+
});
|
|
2228
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
|
|
2229
|
+
kind: "operation-removed",
|
|
2230
|
+
serviceName,
|
|
2231
|
+
operationName: name,
|
|
2232
|
+
classification: "breaking"
|
|
2233
|
+
});
|
|
2234
|
+
for (const [name, newOp] of newByName) {
|
|
2235
|
+
const oldOp = oldByName.get(name);
|
|
2236
|
+
if (!oldOp) continue;
|
|
2237
|
+
const paramChanges = diffParams(oldOp, newOp);
|
|
2238
|
+
const responseChanged = !typeRefsEqual(oldOp.response, newOp.response);
|
|
2239
|
+
const requestBodyChanged = !requestBodiesEqual(oldOp.requestBody, newOp.requestBody);
|
|
2240
|
+
const httpMethodChanged = oldOp.httpMethod !== newOp.httpMethod;
|
|
2241
|
+
const pathChanged = oldOp.path !== newOp.path;
|
|
2242
|
+
const paginatedChanged = !!oldOp.pagination !== !!newOp.pagination;
|
|
2243
|
+
const injectIdempotencyKeyChanged = oldOp.injectIdempotencyKey !== newOp.injectIdempotencyKey;
|
|
2244
|
+
const errorsDiff = classifyErrorsChange(oldOp.errors, newOp.errors);
|
|
2245
|
+
const errorsChanged = errorsDiff !== "none";
|
|
2246
|
+
if (paramChanges.length > 0 || responseChanged || requestBodyChanged || httpMethodChanged || pathChanged || paginatedChanged || injectIdempotencyKeyChanged || errorsChanged) {
|
|
2247
|
+
const paginatedBreaking = paginatedChanged && !newOp.pagination;
|
|
2248
|
+
const injectIdempotencyKeyBreaking = injectIdempotencyKeyChanged && !newOp.injectIdempotencyKey;
|
|
2249
|
+
const hasBreaking = responseChanged || requestBodyChanged || httpMethodChanged || pathChanged || paginatedBreaking || injectIdempotencyKeyBreaking || errorsDiff === "breaking" || paramChanges.some((pc) => pc.classification === "breaking");
|
|
2250
|
+
changes.push({
|
|
2251
|
+
kind: "operation-modified",
|
|
2252
|
+
serviceName,
|
|
2253
|
+
operationName: name,
|
|
2254
|
+
paramChanges,
|
|
2255
|
+
responseChanged,
|
|
2256
|
+
requestBodyChanged,
|
|
2257
|
+
httpMethodChanged,
|
|
2258
|
+
pathChanged,
|
|
2259
|
+
paginatedChanged,
|
|
2260
|
+
injectIdempotencyKeyChanged,
|
|
2261
|
+
errorsChanged,
|
|
2262
|
+
classification: hasBreaking ? "breaking" : "additive"
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return changes;
|
|
2267
|
+
}
|
|
2268
|
+
function requestBodiesEqual(a, b) {
|
|
2269
|
+
if (!a && !b) return true;
|
|
2270
|
+
if (!a || !b) return false;
|
|
2271
|
+
return typeRefsEqual(a, b);
|
|
2272
|
+
}
|
|
2273
|
+
function classifyErrorsChange(oldErrors, newErrors) {
|
|
2274
|
+
const oldByCode = new Map(oldErrors.map((e) => [e.statusCode, e]));
|
|
2275
|
+
const newByCode = new Map(newErrors.map((e) => [e.statusCode, e]));
|
|
2276
|
+
let hasAdditive = false;
|
|
2277
|
+
let hasBreaking = false;
|
|
2278
|
+
for (const code of newByCode.keys()) if (!oldByCode.has(code)) hasAdditive = true;
|
|
2279
|
+
for (const code of oldByCode.keys()) if (!newByCode.has(code)) hasBreaking = true;
|
|
2280
|
+
for (const [code, newErr] of newByCode) {
|
|
2281
|
+
const oldErr = oldByCode.get(code);
|
|
2282
|
+
if (!oldErr) continue;
|
|
2283
|
+
if (!oldErr.type && !newErr.type) continue;
|
|
2284
|
+
if (!oldErr.type || !newErr.type || !typeRefsEqual(oldErr.type, newErr.type)) hasBreaking = true;
|
|
2285
|
+
}
|
|
2286
|
+
if (hasBreaking) return "breaking";
|
|
2287
|
+
if (hasAdditive) return "additive";
|
|
2288
|
+
return "none";
|
|
2289
|
+
}
|
|
2290
|
+
function diffParams(oldOp, newOp) {
|
|
2291
|
+
const changes = [];
|
|
2292
|
+
const allOldParams = [
|
|
2293
|
+
...oldOp.pathParams,
|
|
2294
|
+
...oldOp.queryParams,
|
|
2295
|
+
...oldOp.headerParams
|
|
2296
|
+
];
|
|
2297
|
+
const allNewParams = [
|
|
2298
|
+
...newOp.pathParams,
|
|
2299
|
+
...newOp.queryParams,
|
|
2300
|
+
...newOp.headerParams
|
|
2301
|
+
];
|
|
2302
|
+
const oldByName = new Map(allOldParams.map((p) => [p.name, p]));
|
|
2303
|
+
const newByName = new Map(allNewParams.map((p) => [p.name, p]));
|
|
2304
|
+
for (const [name, param] of newByName) if (!oldByName.has(name)) changes.push(classifyParamChange("param-added", name, param.required));
|
|
2305
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push(classifyParamChange("param-removed", name));
|
|
2306
|
+
for (const [name, newParam] of newByName) {
|
|
2307
|
+
const oldParam = oldByName.get(name);
|
|
2308
|
+
if (!oldParam) continue;
|
|
2309
|
+
if (!typeRefsEqual(oldParam.type, newParam.type)) changes.push(classifyParamChange("param-type-changed", name));
|
|
2310
|
+
if (oldParam.required !== newParam.required) changes.push(classifyParamChange("param-required-changed", name, newParam.required));
|
|
2311
|
+
}
|
|
2312
|
+
return changes;
|
|
2313
|
+
}
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/differ/services.ts
|
|
2316
|
+
function diffServices(oldServices, newServices) {
|
|
2317
|
+
const changes = [];
|
|
2318
|
+
const oldByName = new Map(oldServices.map((s) => [s.name, s]));
|
|
2319
|
+
const newByName = new Map(newServices.map((s) => [s.name, s]));
|
|
2320
|
+
for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
|
|
2321
|
+
kind: "service-added",
|
|
2322
|
+
name,
|
|
2323
|
+
classification: "additive"
|
|
2324
|
+
});
|
|
2325
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
|
|
2326
|
+
kind: "service-removed",
|
|
2327
|
+
name,
|
|
2328
|
+
classification: "breaking"
|
|
2329
|
+
});
|
|
2330
|
+
for (const [name, newService] of newByName) {
|
|
2331
|
+
const oldService = oldByName.get(name);
|
|
2332
|
+
if (!oldService) continue;
|
|
2333
|
+
changes.push(...diffOperations(name, oldService.operations, newService.operations));
|
|
2334
|
+
}
|
|
2335
|
+
return changes;
|
|
2336
|
+
}
|
|
2337
|
+
//#endregion
|
|
2338
|
+
//#region src/differ/diff.ts
|
|
2339
|
+
function diffSpecs(oldSpec, newSpec) {
|
|
2340
|
+
const changes = [
|
|
2341
|
+
...diffModels(oldSpec.models, newSpec.models),
|
|
2342
|
+
...diffEnums(oldSpec.enums, newSpec.enums),
|
|
2343
|
+
...diffServices(oldSpec.services, newSpec.services)
|
|
2344
|
+
];
|
|
2345
|
+
return {
|
|
2346
|
+
oldVersion: oldSpec.version,
|
|
2347
|
+
newVersion: newSpec.version,
|
|
2348
|
+
changes,
|
|
2349
|
+
summary: summarize(changes)
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
function diffEnums(oldEnums, newEnums) {
|
|
2353
|
+
const changes = [];
|
|
2354
|
+
const oldByName = new Map(oldEnums.map((e) => [e.name, e]));
|
|
2355
|
+
const newByName = new Map(newEnums.map((e) => [e.name, e]));
|
|
2356
|
+
for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
|
|
2357
|
+
kind: "enum-added",
|
|
2358
|
+
name,
|
|
2359
|
+
classification: "additive"
|
|
2360
|
+
});
|
|
2361
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
|
|
2362
|
+
kind: "enum-removed",
|
|
2363
|
+
name,
|
|
2364
|
+
classification: "breaking"
|
|
2365
|
+
});
|
|
2366
|
+
for (const [name, newEnum] of newByName) {
|
|
2367
|
+
const oldEnum = oldByName.get(name);
|
|
2368
|
+
if (!oldEnum) continue;
|
|
2369
|
+
const valueChanges = diffEnumValues(oldEnum, newEnum);
|
|
2370
|
+
if (valueChanges.length > 0) {
|
|
2371
|
+
const hasBreaking = valueChanges.some((vc) => vc.classification === "breaking");
|
|
2372
|
+
changes.push({
|
|
2373
|
+
kind: "enum-modified",
|
|
2374
|
+
name,
|
|
2375
|
+
valueChanges,
|
|
2376
|
+
classification: hasBreaking ? "breaking" : "additive"
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return changes;
|
|
2381
|
+
}
|
|
2382
|
+
function diffEnumValues(oldEnum, newEnum) {
|
|
2383
|
+
const changes = [];
|
|
2384
|
+
const oldByName = new Map(oldEnum.values.map((v) => [v.name, v]));
|
|
2385
|
+
const newByName = new Map(newEnum.values.map((v) => [v.name, v]));
|
|
2386
|
+
for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
|
|
2387
|
+
kind: "value-added",
|
|
2388
|
+
valueName: name,
|
|
2389
|
+
classification: "additive"
|
|
2390
|
+
});
|
|
2391
|
+
for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
|
|
2392
|
+
kind: "value-removed",
|
|
2393
|
+
valueName: name,
|
|
2394
|
+
classification: "breaking"
|
|
2395
|
+
});
|
|
2396
|
+
for (const [name, newVal] of newByName) {
|
|
2397
|
+
const oldVal = oldByName.get(name);
|
|
2398
|
+
if (!oldVal) continue;
|
|
2399
|
+
if (oldVal.value !== newVal.value) changes.push({
|
|
2400
|
+
kind: "value-changed",
|
|
2401
|
+
valueName: name,
|
|
2402
|
+
classification: "breaking",
|
|
2403
|
+
details: `value changed from '${oldVal.value}' to '${newVal.value}'`
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
return changes;
|
|
2407
|
+
}
|
|
2408
|
+
function summarize(changes) {
|
|
2409
|
+
let added = 0;
|
|
2410
|
+
let removed = 0;
|
|
2411
|
+
let modified = 0;
|
|
2412
|
+
let breaking = 0;
|
|
2413
|
+
let additive = 0;
|
|
2414
|
+
for (const c of changes) {
|
|
2415
|
+
if (c.kind.endsWith("-added")) added++;
|
|
2416
|
+
else if (c.kind.endsWith("-removed")) removed++;
|
|
2417
|
+
else if (c.kind.endsWith("-modified")) modified++;
|
|
2418
|
+
if (c.classification === "breaking") breaking++;
|
|
2419
|
+
else additive++;
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
added,
|
|
2423
|
+
removed,
|
|
2424
|
+
modified,
|
|
2425
|
+
breaking,
|
|
2426
|
+
additive
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
//#endregion
|
|
2430
|
+
export { integrateGeneratedFiles as a, applyFileHeaders as c, generateAllFiles as d, generateFiles as f, generate as i, buildEmitterContext as l, getEmitter as n, mapFilesForTargetIntegration as o, registerEmitter as r, treeShakeFiles as s, diffSpecs as t, collectReferencedNames as u };
|
|
2431
|
+
|
|
2432
|
+
//# sourceMappingURL=diff-Cn6S74vV.mjs.map
|