hivecode-mcp 0.7.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -84
- package/core.js +62 -13
- package/hive-mcp.js +515 -45
- package/icr.js +506 -34
- package/lang-brace.js +240 -0
- package/lang-js.js +642 -418
- package/lang-json.js +184 -0
- package/lang-python.js +360 -0
- package/lang-ruby.js +230 -0
- package/lang-toml.js +136 -0
- package/lang-yaml.js +151 -0
- package/package.json +97 -62
- package/substrate.js +252 -0
- package/sync.js +642 -13
- package/MCP.md +0 -77
package/lang-js.js
CHANGED
|
@@ -1,418 +1,642 @@
|
|
|
1
|
-
// lang-js.js
|
|
2
|
-
//
|
|
3
|
-
// ICR's merge engine (icr.js) is language-agnostic: it merges a list of keyed "units"
|
|
4
|
-
// and asks intent questions ("what names are declared / used / what's a function body").
|
|
5
|
-
// Everything that actually knows about a *language*
|
|
6
|
-
// declaration is, how to find references
|
|
7
|
-
//
|
|
8
|
-
// To add a language (Python, Go, Rust
|
|
9
|
-
// and register it with icr.js's registerLanguage(). This JS provider is just the first
|
|
10
|
-
// plugin; it happens to use `acorn` because acorn is a tiny pure-JS parser. A tree-sitter
|
|
11
|
-
// provider would implement the identical interface over tree-sitter grammars.
|
|
12
|
-
|
|
13
|
-
import * as acorn from 'acorn'
|
|
14
|
-
|
|
15
|
-
// Parse permissively: try module syntax, fall back to script (so plain snippets and
|
|
16
|
-
// ESM both work). Throws only when the code genuinely won't parse either way.
|
|
17
|
-
function
|
|
18
|
-
try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
|
|
19
|
-
catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return null
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1
|
+
// lang-js.js — the JavaScript language provider for ICR.
|
|
2
|
+
//
|
|
3
|
+
// ICR's merge engine (icr.js) is language-agnostic: it merges a list of keyed "units"
|
|
4
|
+
// and asks intent questions ("what names are declared / used / what's a function body").
|
|
5
|
+
// Everything that actually knows about a *language* — how to parse it, what a top-level
|
|
6
|
+
// declaration is, how to find references — lives behind a provider like this one.
|
|
7
|
+
//
|
|
8
|
+
// To add a language (Python, Go, Rust…), write another module exposing the same shape
|
|
9
|
+
// and register it with icr.js's registerLanguage(). This JS provider is just the first
|
|
10
|
+
// plugin; it happens to use `acorn` because acorn is a tiny pure-JS parser. A tree-sitter
|
|
11
|
+
// provider would implement the identical interface over tree-sitter grammars.
|
|
12
|
+
|
|
13
|
+
import * as acorn from 'acorn'
|
|
14
|
+
|
|
15
|
+
// Parse permissively: try module syntax, fall back to script (so plain snippets and
|
|
16
|
+
// ESM both work). Throws only when the code genuinely won't parse either way.
|
|
17
|
+
function parseRaw(src) {
|
|
18
|
+
try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
|
|
19
|
+
catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tiny parse cache. One structuralMerge re-parses the SAME three texts (and the merged
|
|
23
|
+
// result) many times across its tiers — gate, units, declaredNames, scope analysis,
|
|
24
|
+
// final validation. Caching by exact text collapses that to one parse per distinct
|
|
25
|
+
// version (a 1MB-file merge dropped ~7.6s → ~3s). Small and bounded: 8 entries, LRU.
|
|
26
|
+
// String comparison is a cheap memcmp; ASTs are treated as immutable by all callers.
|
|
27
|
+
const PARSE_CACHE = new Map() // src -> ast (Map preserves insertion order → LRU)
|
|
28
|
+
function parse(src) {
|
|
29
|
+
const hit = PARSE_CACHE.get(src)
|
|
30
|
+
if (hit) { PARSE_CACHE.delete(src); PARSE_CACHE.set(src, hit); return hit } // refresh LRU position
|
|
31
|
+
const ast = parseRaw(src)
|
|
32
|
+
PARSE_CACHE.set(src, ast)
|
|
33
|
+
if (PARSE_CACHE.size > 8) PARSE_CACHE.delete(PARSE_CACHE.keys().next().value)
|
|
34
|
+
return ast
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parses(src) { try { parse(src); return true } catch { return false } }
|
|
38
|
+
|
|
39
|
+
// A stable key per top-level node — what lets us recognize "the same declaration"
|
|
40
|
+
// across two edits. Names where we have them; position as a last resort.
|
|
41
|
+
// Dotted source path of a simple expression target (Axios.prototype.request,
|
|
42
|
+
// utils.forEach, this.config) — or null when it isn't a plain dotted path.
|
|
43
|
+
function exprPath(node) {
|
|
44
|
+
if (!node) return null
|
|
45
|
+
if (node.type === 'Identifier') return node.name
|
|
46
|
+
if (node.type === 'ThisExpression') return 'this'
|
|
47
|
+
if (node.type === 'MemberExpression' && !node.computed) {
|
|
48
|
+
const o = exprPath(node.object)
|
|
49
|
+
return o && node.property && node.property.name ? o + '.' + node.property.name : null
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// CONTENT-ANCHORED key for a statement, or null for truly anonymous ones.
|
|
55
|
+
// Index keys (stmt:N) are a last resort ONLY: an insertion above an index-keyed
|
|
56
|
+
// statement shifts every index, so unrelated statements pair up as fake
|
|
57
|
+
// same-unit conflicts — and deletions mispair so deleted code can resurrect.
|
|
58
|
+
// (Both failure modes were found by the merge census replaying axios/express
|
|
59
|
+
// history.) Assignments key by their target, calls by callee path plus a
|
|
60
|
+
// leading string literal (`it('name')`, `describe('name')`), directives by text.
|
|
61
|
+
function baseKeyOf(node) {
|
|
62
|
+
let n = node
|
|
63
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
64
|
+
if (n.type === 'ExportDefaultDeclaration') return 'export:default'
|
|
65
|
+
if (n.type === 'FunctionDeclaration' && n.id) return 'fn:' + n.id.name
|
|
66
|
+
if (n.type === 'ClassDeclaration' && n.id) return 'class:' + n.id.name
|
|
67
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name)
|
|
68
|
+
return 'var:' + n.declarations[0].id.name
|
|
69
|
+
// Key imports by their SOURCE MODULE, so two agents adding imports from DIFFERENT
|
|
70
|
+
// modules never collide, and two touching the SAME module are merged (specifier union).
|
|
71
|
+
if (n.type === 'ImportDeclaration') return 'import:' + (n.source && n.source.value != null ? n.source.value : '?')
|
|
72
|
+
// Specifier-only exports (`export { a, b }`, `export { x } from './m'`, `export * from …`)
|
|
73
|
+
// carry no declaration, so without a stable key they fall to positional stmt:N — and an
|
|
74
|
+
// edit to the export list then mispairs, DUPLICATES the statement, and knocks the whole
|
|
75
|
+
// merge to the line tier (which silently skips the intent/dangling checks). A stable key
|
|
76
|
+
// pairs them so an unedited side is kept and an edited side wins cleanly.
|
|
77
|
+
if (n.type === 'ExportNamedDeclaration') return n.source && n.source.value != null ? 'export-from:' + n.source.value : 'export-names'
|
|
78
|
+
if (n.type === 'ExportAllDeclaration') return 'export-all:' + (n.source && n.source.value != null ? n.source.value : '?')
|
|
79
|
+
if (n.type === 'ExpressionStatement') {
|
|
80
|
+
const e = n.expression
|
|
81
|
+
if (e.type === 'Literal' && typeof e.value === 'string') return 'directive:' + e.value // 'use strict'
|
|
82
|
+
if (e.type === 'AssignmentExpression') { const t = exprPath(e.left); if (t) return 'assign:' + t }
|
|
83
|
+
if (e.type === 'CallExpression') {
|
|
84
|
+
const t = exprPath(e.callee)
|
|
85
|
+
if (t) {
|
|
86
|
+
const a0 = e.arguments && e.arguments[0]
|
|
87
|
+
return 'call:' + t + (a0 && a0.type === 'Literal' && typeof a0.value === 'string' ? ':' + a0.value : '')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Key an ordered statement list: content-anchored where possible (duplicates get
|
|
95
|
+
// #1, #2… by occurrence), positional stmt:N only for the anonymous remainder.
|
|
96
|
+
function keyStatements(nodes) {
|
|
97
|
+
const seen = new Map()
|
|
98
|
+
return nodes.map((n, i) => {
|
|
99
|
+
const k = baseKeyOf(n)
|
|
100
|
+
if (k == null) return 'stmt:' + i
|
|
101
|
+
const c = seen.get(k) || 0
|
|
102
|
+
seen.set(k, c + 1)
|
|
103
|
+
return c ? k + '#' + c : k
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function keyOf(node, i) { const k = baseKeyOf(node); return k == null ? 'stmt:' + i : k }
|
|
108
|
+
|
|
109
|
+
// Top-level units: ordered { key, text, start, end } for each declaration/statement.
|
|
110
|
+
// start/end are byte offsets in `src` — used by the format-preserving splice so unchanged
|
|
111
|
+
// regions (and the whitespace/comments between units) survive a merge verbatim.
|
|
112
|
+
function units(src) {
|
|
113
|
+
const ast = parse(src)
|
|
114
|
+
const keys = keyStatements(ast.body)
|
|
115
|
+
return ast.body.map((node, i) => ({ key: keys[i], text: src.slice(node.start, node.end), start: node.start, end: node.end }))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Bare declared name (foo, Bar, VERSION) of a node, or null for anonymous statements.
|
|
119
|
+
function bareName(node) {
|
|
120
|
+
let n = node
|
|
121
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
122
|
+
if (n.type === 'FunctionDeclaration' && n.id) return n.id.name
|
|
123
|
+
if (n.type === 'ClassDeclaration' && n.id) return n.id.name
|
|
124
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name) return n.declarations[0].id.name
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function declaredNames(src) {
|
|
129
|
+
const s = new Set()
|
|
130
|
+
for (const node of parse(src).body) {
|
|
131
|
+
const nm = bareName(node); if (nm) s.add(nm)
|
|
132
|
+
// Import locals count as declared names too — so if an import is ever dropped and its
|
|
133
|
+
// binding is still used, the dangling-reference check catches it (safety net for the
|
|
134
|
+
// import merge). Keeps the never-emit-broken-code guarantee honest for imports.
|
|
135
|
+
if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
|
|
136
|
+
}
|
|
137
|
+
return s
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- import-aware merging -------------------------------------------------------
|
|
141
|
+
// Parse a single import statement into its parts, or null if it isn't one.
|
|
142
|
+
function parseImport(src) {
|
|
143
|
+
let n
|
|
144
|
+
try { n = parse(src).body[0] } catch { return null }
|
|
145
|
+
if (!n || n.type !== 'ImportDeclaration') return null
|
|
146
|
+
let def = null, ns = null
|
|
147
|
+
const named = []
|
|
148
|
+
for (const sp of n.specifiers) {
|
|
149
|
+
if (sp.type === 'ImportDefaultSpecifier') def = sp.local.name
|
|
150
|
+
else if (sp.type === 'ImportNamespaceSpecifier') ns = sp.local.name
|
|
151
|
+
else named.push({ imported: sp.imported.name != null ? sp.imported.name : sp.imported.value, local: sp.local.name })
|
|
152
|
+
}
|
|
153
|
+
return { source: n.source.value, def, ns, named }
|
|
154
|
+
}
|
|
155
|
+
function renderImport({ source, def, ns, named }) {
|
|
156
|
+
const parts = []
|
|
157
|
+
if (def) parts.push(def)
|
|
158
|
+
if (ns) parts.push('* as ' + ns)
|
|
159
|
+
if (named.length) parts.push('{ ' + named.map((s) => s.imported === s.local ? s.local : s.imported + ' as ' + s.local).join(', ') + ' }')
|
|
160
|
+
if (!parts.length) return "import '" + source + "'"
|
|
161
|
+
return 'import ' + parts.join(', ') + " from '" + source + "'"
|
|
162
|
+
}
|
|
163
|
+
// Both sides changed an import from the SAME module → union their specifiers into one
|
|
164
|
+
// statement (the common "both agents added an import" case). Returns null — meaning
|
|
165
|
+
// "let the normal conflict path handle it" — for anything we can't safely combine
|
|
166
|
+
// (different modules, clashing defaults/namespaces, namespace-mixed-with-named).
|
|
167
|
+
function mergeUnit(baseText, aText, bText) {
|
|
168
|
+
const a = parseImport(aText), b = parseImport(bText)
|
|
169
|
+
if (!a || !b || a.source !== b.source) return null
|
|
170
|
+
let def
|
|
171
|
+
if (a.def && b.def) { if (a.def !== b.def) return null; def = a.def } else def = a.def || b.def
|
|
172
|
+
let ns
|
|
173
|
+
if (a.ns && b.ns) { if (a.ns !== b.ns) return null; ns = a.ns } else ns = a.ns || b.ns
|
|
174
|
+
if (ns && (a.named.length || b.named.length)) return null // `* as X` can't share a statement with named imports
|
|
175
|
+
const seen = new Set(), named = []
|
|
176
|
+
for (const s of [...a.named, ...b.named]) { const k = s.imported + '|' + s.local; if (!seen.has(k)) { seen.add(k); named.push(s) } }
|
|
177
|
+
// Canonical specifier order: the union must not depend on which side is `a` — the merge
|
|
178
|
+
// has to be SYMMETRIC (merge(a,b) === merge(b,a)) or two live peers never settle on one
|
|
179
|
+
// byte-form. (Found by the property fuzzer: a 1-in-40k NOT-SYMMETRIC violation.)
|
|
180
|
+
named.sort((x, y) => (x.imported + '|' + x.local).localeCompare(y.imported + '|' + y.local))
|
|
181
|
+
return renderImport({ source: a.source, def, ns, named })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The BODY of a top-level declaration, by name, with the name itself excluded — so two
|
|
185
|
+
// declarations that differ only in their name compare equal (that's how we spot a rename).
|
|
186
|
+
function declBody(src, name) {
|
|
187
|
+
for (const node of parse(src).body) {
|
|
188
|
+
if (bareName(node) !== name) continue
|
|
189
|
+
let n = node
|
|
190
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
191
|
+
if ((n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') && n.body)
|
|
192
|
+
return src.slice(n.body.start, n.body.end)
|
|
193
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
|
|
194
|
+
const init = n.declarations[0].init
|
|
195
|
+
return src.slice(init.start, init.end)
|
|
196
|
+
}
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if top-level declaration `name` is declared `async`
|
|
203
|
+
function isAsyncDecl(src, name) {
|
|
204
|
+
try {
|
|
205
|
+
for (const node of parse(src).body) {
|
|
206
|
+
if (bareName(node) !== name) continue
|
|
207
|
+
let n = node
|
|
208
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
209
|
+
if (n.type === 'ExportDefaultDeclaration' && n.declaration) n = n.declaration
|
|
210
|
+
if (n.type === 'FunctionDeclaration') return !!n.async
|
|
211
|
+
if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
|
|
212
|
+
const init = n.declarations[0].init
|
|
213
|
+
if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
|
|
214
|
+
return !!init.async
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {}
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Upgrade free call sites of `fnName` to `await fnName(...)` if they are inside an async parent
|
|
223
|
+
// and not already awaited.
|
|
224
|
+
function upgradeCallSitesToAwait(src, fnName) {
|
|
225
|
+
try {
|
|
226
|
+
const ast = parse(src)
|
|
227
|
+
const spots = []
|
|
228
|
+
|
|
229
|
+
function walkAST(node, parent, isAsyncScope = false) {
|
|
230
|
+
if (!node || typeof node.type !== 'string') return
|
|
231
|
+
|
|
232
|
+
let childAsyncScope = isAsyncScope
|
|
233
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
234
|
+
childAsyncScope = !!node.async
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (node.type === 'CallExpression') {
|
|
238
|
+
let isTarget = false
|
|
239
|
+
if (node.callee.type === 'Identifier' && node.callee.name === fnName) isTarget = true
|
|
240
|
+
if (isTarget && childAsyncScope) {
|
|
241
|
+
const alreadyAwaited = parent && parent.type === 'AwaitExpression'
|
|
242
|
+
if (!alreadyAwaited) {
|
|
243
|
+
spots.push(node.start)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const k of Object.keys(node)) {
|
|
249
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
|
|
250
|
+
const v = node[k]
|
|
251
|
+
if (Array.isArray(v)) {
|
|
252
|
+
for (const c of v) walkAST(c, node, childAsyncScope)
|
|
253
|
+
} else if (v && typeof v.type === 'string') {
|
|
254
|
+
walkAST(v, node, childAsyncScope)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
walkAST(ast, null, false)
|
|
260
|
+
if (!spots.length) return src
|
|
261
|
+
|
|
262
|
+
spots.sort((a, b) => b - a)
|
|
263
|
+
let out = src
|
|
264
|
+
for (const pos of spots) {
|
|
265
|
+
out = out.slice(0, pos) + 'await ' + out.slice(pos)
|
|
266
|
+
}
|
|
267
|
+
return out
|
|
268
|
+
} catch {
|
|
269
|
+
return src
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find callers of `fnName` that are synchronous (cannot be auto-awaited without changing the caller's signature)
|
|
274
|
+
function syncCallSitesOf(src, fnName) {
|
|
275
|
+
const syncCallers = []
|
|
276
|
+
try {
|
|
277
|
+
const ast = parse(src)
|
|
278
|
+
function walkAST(node, currentFnName = null, isAsyncScope = false) {
|
|
279
|
+
if (!node || typeof node.type !== 'string') return
|
|
280
|
+
|
|
281
|
+
let childAsyncScope = isAsyncScope
|
|
282
|
+
let fnNameContext = currentFnName
|
|
283
|
+
if (node.type === 'FunctionDeclaration') {
|
|
284
|
+
childAsyncScope = !!node.async
|
|
285
|
+
fnNameContext = node.id ? node.id.name : '<anonymous>'
|
|
286
|
+
} else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
287
|
+
childAsyncScope = !!node.async
|
|
288
|
+
if (node.id) fnNameContext = node.id.name
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (node.type === 'CallExpression') {
|
|
292
|
+
if (node.callee.type === 'Identifier' && node.callee.name === fnName) {
|
|
293
|
+
if (!childAsyncScope) {
|
|
294
|
+
syncCallers.push(fnNameContext || '<top-level>')
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const k of Object.keys(node)) {
|
|
300
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
|
|
301
|
+
const v = node[k]
|
|
302
|
+
if (Array.isArray(v)) {
|
|
303
|
+
for (const c of v) walkAST(c, fnNameContext, childAsyncScope)
|
|
304
|
+
} else if (v && typeof v.type === 'string') {
|
|
305
|
+
walkAST(v, fnNameContext, childAsyncScope)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
walkAST(ast, null, false)
|
|
310
|
+
} catch {}
|
|
311
|
+
return syncCallers
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Rewrite every reference to `oldName` as `newName` (skipping property/key positions,
|
|
315
|
+
// which aren't references to the declaration). Edits back-to-front to keep offsets valid.
|
|
316
|
+
function renameRefs(src, oldName, newName) {
|
|
317
|
+
const spots = []
|
|
318
|
+
walk(parse(src), (node, parent, key) => {
|
|
319
|
+
if (node.type !== 'Identifier' || node.name !== oldName) return
|
|
320
|
+
if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
|
|
321
|
+
if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
|
|
322
|
+
spots.push([node.start, node.end])
|
|
323
|
+
})
|
|
324
|
+
spots.sort((a, b) => b[0] - a[0])
|
|
325
|
+
let out = src
|
|
326
|
+
for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
|
|
327
|
+
return out
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Every identifier USED anywhere (skips obj.prop names and non-computed object keys).
|
|
331
|
+
// Approximate — good enough for the intent check; the real version is scope-aware.
|
|
332
|
+
function usedIdentifiers(src) {
|
|
333
|
+
const used = new Set()
|
|
334
|
+
walk(parse(src), (node, parent, key) => {
|
|
335
|
+
if (node.type !== 'Identifier') return
|
|
336
|
+
if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
|
|
337
|
+
if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
|
|
338
|
+
used.add(node.name)
|
|
339
|
+
})
|
|
340
|
+
return used
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function walk(node, visit, parent = null, key = null) {
|
|
344
|
+
if (!node || typeof node.type !== 'string') return
|
|
345
|
+
visit(node, parent, key)
|
|
346
|
+
for (const k of Object.keys(node)) {
|
|
347
|
+
if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') continue
|
|
348
|
+
const v = node[k]
|
|
349
|
+
if (Array.isArray(v)) { for (const c of v) walk(c, visit, node, k) }
|
|
350
|
+
else if (v && typeof v.type === 'string') walk(v, visit, node, k)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- scope-aware reference analysis ---------------------------------------------
|
|
355
|
+
// The names REFERENCED in `src` that resolve to NOTHING in the file's own scopes —
|
|
356
|
+
// i.e. free/global names, including any use of a top-level declaration. This is the
|
|
357
|
+
// scope-aware upgrade of usedIdentifiers: a use of `x` that resolves to a LOCAL binding
|
|
358
|
+
// (a param, a `const x` in the same function, a catch var, a loop var…) is NOT counted,
|
|
359
|
+
// because it isn't a reference to a top-level declaration. That lets ICR tell a deleted
|
|
360
|
+
// top-level `helper` apart from an unrelated local `helper` that merely shares its name.
|
|
361
|
+
//
|
|
362
|
+
// Honest scope coverage: functions (declaration/expression/arrow), blocks, catch
|
|
363
|
+
// clauses, for/for-in/for-of loop bindings, params with simple destructuring, imports,
|
|
364
|
+
// var-hoisting to the function scope, and let/const/class/function block binding.
|
|
365
|
+
// Approximations (rare, and they only ever make the dangling check MORE cautious):
|
|
366
|
+
// default-value expressions in patterns and `with`/eval are not modeled.
|
|
367
|
+
// Walk the AST and call emit(identifierNode) for every reference that resolves to NO
|
|
368
|
+
// local binding (free / would resolve to module or global scope). Both the dangling-ref
|
|
369
|
+
// check and scope-aware renaming are built on this single traversal.
|
|
370
|
+
function walkFreeRefs(src, emit) { resolveScopes(parse(src), null, emit) }
|
|
371
|
+
|
|
372
|
+
function referencedFreeNames(src) {
|
|
373
|
+
const free = new Set()
|
|
374
|
+
walkFreeRefs(src, (node) => free.add(node.name))
|
|
375
|
+
return free
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Rename ONLY the free references to oldName (those that resolve to the top-level/global
|
|
379
|
+
// binding) — never a local variable that merely shares the name. Edits back-to-front.
|
|
380
|
+
function renameFreeRefs(src, oldName, newName) {
|
|
381
|
+
const spots = []
|
|
382
|
+
walkFreeRefs(src, (node) => { if (node.name === oldName) spots.push([node.start, node.end]) })
|
|
383
|
+
spots.sort((a, b) => b[0] - a[0])
|
|
384
|
+
let out = src
|
|
385
|
+
for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
|
|
386
|
+
return out
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const CHILD_SKIP = new Set(['type', 'start', 'end', 'loc', 'range'])
|
|
390
|
+
function eachChild(node, fn) {
|
|
391
|
+
for (const k of Object.keys(node)) {
|
|
392
|
+
if (CHILD_SKIP.has(k)) continue
|
|
393
|
+
const v = node[k]
|
|
394
|
+
if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') fn(c) }
|
|
395
|
+
else if (v && typeof v.type === 'string') fn(v)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Names introduced by a binding target (handles identifiers + simple destructuring).
|
|
400
|
+
function bindingNames(node, out) {
|
|
401
|
+
if (!node) return
|
|
402
|
+
switch (node.type) {
|
|
403
|
+
case 'Identifier': out.add(node.name); break
|
|
404
|
+
case 'ObjectPattern': for (const p of node.properties) bindingNames(p.type === 'RestElement' ? p.argument : p.value, out); break
|
|
405
|
+
case 'ArrayPattern': for (const el of node.elements) if (el) bindingNames(el, out); break
|
|
406
|
+
case 'AssignmentPattern': bindingNames(node.left, out); break
|
|
407
|
+
case 'RestElement': bindingNames(node.argument, out); break
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// A binding target's NAMES are bindings, but its default values (`a = expr`) and computed
|
|
412
|
+
// destructuring keys (`{ [k]: v }`) are REFERENCES that must be resolved against the scope.
|
|
413
|
+
// Without this, a name used only in a default value would be missed by the dangling check.
|
|
414
|
+
function resolveBindingDefaults(node, scope, emit) {
|
|
415
|
+
if (!node) return
|
|
416
|
+
switch (node.type) {
|
|
417
|
+
case 'AssignmentPattern': resolveBindingDefaults(node.left, scope, emit); resolveScopes(node.right, scope, emit); break
|
|
418
|
+
case 'ObjectPattern': for (const p of node.properties) {
|
|
419
|
+
if (p.type === 'RestElement') { resolveBindingDefaults(p.argument, scope, emit); break }
|
|
420
|
+
if (p.computed) resolveScopes(p.key, scope, emit)
|
|
421
|
+
resolveBindingDefaults(p.value, scope, emit)
|
|
422
|
+
} break
|
|
423
|
+
case 'ArrayPattern': for (const el of node.elements) if (el) resolveBindingDefaults(el, scope, emit); break
|
|
424
|
+
case 'RestElement': resolveBindingDefaults(node.argument, scope, emit); break
|
|
425
|
+
// Identifier: a pure binding — nothing to resolve.
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const makeScope = (parent, fnScope) => { const s = { vars: new Set(), parent }; s.fnScope = fnScope || s; return s }
|
|
430
|
+
const resolves = (scope, name) => { for (let s = scope; s; s = s.parent) if (s.vars.has(name)) return true; return false }
|
|
431
|
+
|
|
432
|
+
// Hoist var declarations + function declarations into the function scope (deep, but not
|
|
433
|
+
// crossing into nested functions, which own their own var scope).
|
|
434
|
+
function hoistFunctionScope(nodes, scope) { for (const n of nodes) collectHoisted(n, scope) }
|
|
435
|
+
function collectHoisted(node, scope) {
|
|
436
|
+
if (!node || typeof node.type !== 'string') return
|
|
437
|
+
if (node.type === 'FunctionDeclaration') { if (node.id) scope.fnScope.vars.add(node.id.name); return }
|
|
438
|
+
if (/Function(Expression)?$|ArrowFunctionExpression/.test(node.type)) return // nested function: stop
|
|
439
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'var')
|
|
440
|
+
for (const d of node.declarations) bindingNames(d.id, scope.fnScope.vars)
|
|
441
|
+
eachChild(node, (c) => collectHoisted(c, scope))
|
|
442
|
+
}
|
|
443
|
+
// Bind let/const/class/function/import names declared directly in a block/program.
|
|
444
|
+
function hoistBlock(nodes, scope) {
|
|
445
|
+
for (const n of nodes) {
|
|
446
|
+
if (n.type === 'VariableDeclaration' && (n.kind === 'let' || n.kind === 'const'))
|
|
447
|
+
for (const d of n.declarations) bindingNames(d.id, scope.vars)
|
|
448
|
+
else if (n.type === 'ClassDeclaration' && n.id) scope.vars.add(n.id.name)
|
|
449
|
+
else if (n.type === 'FunctionDeclaration' && n.id) scope.vars.add(n.id.name)
|
|
450
|
+
else if (n.type === 'ImportDeclaration') for (const sp of n.specifiers) scope.vars.add(sp.local.name)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveScopes(node, scope, emit) {
|
|
455
|
+
if (!node || typeof node.type !== 'string') return
|
|
456
|
+
switch (node.type) {
|
|
457
|
+
case 'Program': {
|
|
458
|
+
const s = makeScope(null, null)
|
|
459
|
+
hoistFunctionScope(node.body, s); hoistBlock(node.body, s)
|
|
460
|
+
for (const c of node.body) resolveScopes(c, s, emit)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': {
|
|
464
|
+
const s = makeScope(scope, null)
|
|
465
|
+
for (const p of node.params) bindingNames(p, s.vars)
|
|
466
|
+
for (const p of node.params) resolveBindingDefaults(p, s, emit) // default values are references
|
|
467
|
+
if (node.id && node.type === 'FunctionExpression') s.vars.add(node.id.name) // named fn expr
|
|
468
|
+
if (node.body.type === 'BlockStatement') {
|
|
469
|
+
hoistFunctionScope(node.body.body, s); hoistBlock(node.body.body, s)
|
|
470
|
+
for (const c of node.body.body) resolveScopes(c, s, emit)
|
|
471
|
+
} else resolveScopes(node.body, s, emit) // arrow with expression body
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
case 'BlockStatement': {
|
|
475
|
+
const s = makeScope(scope, scope.fnScope); hoistBlock(node.body, s)
|
|
476
|
+
for (const c of node.body) resolveScopes(c, s, emit)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
case 'CatchClause': {
|
|
480
|
+
const s = makeScope(scope, scope.fnScope)
|
|
481
|
+
if (node.param) bindingNames(node.param, s.vars)
|
|
482
|
+
resolveScopes(node.body, s, emit)
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
case 'ForStatement': {
|
|
486
|
+
const s = makeScope(scope, scope.fnScope)
|
|
487
|
+
if (node.init && node.init.type === 'VariableDeclaration') hoistBlock([node.init], s)
|
|
488
|
+
for (const k of ['init', 'test', 'update', 'body']) if (node[k]) resolveScopes(node[k], s, emit)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
case 'ForInStatement': case 'ForOfStatement': {
|
|
492
|
+
const s = makeScope(scope, scope.fnScope)
|
|
493
|
+
if (node.left.type === 'VariableDeclaration') hoistBlock([node.left], s)
|
|
494
|
+
else resolveScopes(node.left, s, emit)
|
|
495
|
+
resolveScopes(node.right, s, emit); resolveScopes(node.body, s, emit)
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
case 'VariableDeclarator': { resolveBindingDefaults(node.id, scope, emit); if (node.init) resolveScopes(node.init, scope, emit); return } // id is a binding (defaults are refs)
|
|
499
|
+
case 'Identifier': { if (!resolves(scope, node.name)) emit(node); return }
|
|
500
|
+
case 'MemberExpression': { resolveScopes(node.object, scope, emit); if (node.computed) resolveScopes(node.property, scope, emit); return }
|
|
501
|
+
case 'Property': { if (node.computed) resolveScopes(node.key, scope, emit); resolveScopes(node.value, scope, emit); return }
|
|
502
|
+
case 'PropertyDefinition': case 'MethodDefinition': { if (node.computed) resolveScopes(node.key, scope, emit); if (node.value) resolveScopes(node.value, scope, emit); return }
|
|
503
|
+
case 'LabeledStatement': { resolveScopes(node.body, scope, emit); return }
|
|
504
|
+
case 'BreakStatement': case 'ContinueStatement': return // labels aren't value references
|
|
505
|
+
case 'ExportSpecifier': { resolveScopes(node.local, scope, emit); return }
|
|
506
|
+
case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': return // binding positions
|
|
507
|
+
default: eachChild(node, (c) => resolveScopes(c, scope, emit))
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// A stable key for a class member, so two agents editing DIFFERENT methods of the same
|
|
512
|
+
// class merge, while editing the SAME method conflicts.
|
|
513
|
+
function memberKey(m, i) {
|
|
514
|
+
const kind = m.type === 'MethodDefinition' ? (m.kind === 'constructor' ? 'ctor' : 'method') : 'field'
|
|
515
|
+
if (m.key && m.key.type === 'Identifier' && !m.computed)
|
|
516
|
+
return kind + ':' + (m.static ? 'static.' : '') + m.key.name
|
|
517
|
+
return 'member:' + i
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// For finer-grained merging: split a single declaration into its signature text + keyed
|
|
521
|
+
// inner units, plus how to reassemble them. Handles FUNCTIONS (body statements, keyed
|
|
522
|
+
// like top-level decls) and CLASSES (members, keyed by method/field name). Returns null
|
|
523
|
+
// for anything else (the engine then treats the declaration as indivisible). Tolerates
|
|
524
|
+
// fragments that don't parse standalone (e.g. a bare `return`).
|
|
525
|
+
const MEMBER_WRAP = 'class __ICR__{'
|
|
526
|
+
// A class member (method) doesn't parse standalone — wrap it in a throwaway class so we
|
|
527
|
+
// can recurse INTO a method body that both agents edited. Offsets are mapped back to src.
|
|
528
|
+
function splitMember(src) {
|
|
529
|
+
let ast
|
|
530
|
+
try { ast = parse(MEMBER_WRAP + src + '}') } catch { return null }
|
|
531
|
+
const cls = ast.body[0]
|
|
532
|
+
if (!cls || cls.type !== 'ClassDeclaration' || !cls.body.body.length) return null
|
|
533
|
+
const m = cls.body.body[0]
|
|
534
|
+
if (!m || m.type !== 'MethodDefinition' || !m.value || !m.value.body || m.value.body.type !== 'BlockStatement') return null
|
|
535
|
+
const off = MEMBER_WRAP.length, body = m.value.body
|
|
536
|
+
const bo = body.start - off, bc = body.end - 1 - off // brace-open / brace-close in src coords
|
|
537
|
+
const bs = bo + 1
|
|
538
|
+
const ks = keyStatements(body.body)
|
|
539
|
+
const units = body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start - off, s.end - off), start: (s.start - off) - bs, end: (s.end - off) - bs }))
|
|
540
|
+
return { sig: src.slice(0, bo), open: '{', close: src.slice(bc), body: src.slice(bs, bc), units, join: '\n ', spliceable: true }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function splitUnit(src) {
|
|
544
|
+
let ast
|
|
545
|
+
try { ast = parse(src) } catch { return splitMember(src) } // maybe a class-member fragment
|
|
546
|
+
if (!ast || !ast.body) return splitMember(src) // defensive: a null AST must never reach .body
|
|
547
|
+
let n = ast.body[0]
|
|
548
|
+
if (!n) return null
|
|
549
|
+
// Unwrap export wrappers — but ALWAYS slice signatures from 0, not the inner node's
|
|
550
|
+
// start, or the wrapper text (`export `, `export default `) is silently DROPPED on
|
|
551
|
+
// reassembly. Found in the wild: ICR merging its own source lost `export function`.
|
|
552
|
+
if ((n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration') && n.declaration) n = n.declaration
|
|
553
|
+
if (n.type === 'FunctionDeclaration' && n.body) {
|
|
554
|
+
const bs = n.body.start + 1, be = n.body.end - 1 // between the braces
|
|
555
|
+
const ks = keyStatements(n.body.body)
|
|
556
|
+
const units = n.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
|
|
557
|
+
return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
|
|
558
|
+
}
|
|
559
|
+
if (n.type === 'ClassDeclaration' && n.body) {
|
|
560
|
+
const bs = n.body.start + 1, be = n.body.end - 1
|
|
561
|
+
const units = n.body.body.map((m, i) => ({ key: memberKey(m, i), text: src.slice(m.start, m.end), start: m.start - bs, end: m.end - bs }))
|
|
562
|
+
return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n\n ', spliceable: true }
|
|
563
|
+
}
|
|
564
|
+
// `const x = { ... }` / `const x = () => { ... }` / `const x = function () { ... }`
|
|
565
|
+
// and the CommonJS twins `module.exports = function () { ... }` / `x.y = { ... }` —
|
|
566
|
+
// without descending into assignments, two sides editing DIFFERENT lines of one
|
|
567
|
+
// exported function read as a same-declaration conflict (census overcount on axios).
|
|
568
|
+
let init = null
|
|
569
|
+
if (n.type === 'VariableDeclaration' && n.declarations.length === 1 && n.declarations[0].init) init = n.declarations[0].init
|
|
570
|
+
else if (n.type === 'ExpressionStatement' && n.expression.type === 'AssignmentExpression') init = n.expression.right
|
|
571
|
+
if (init) {
|
|
572
|
+
if (init.type === 'ObjectExpression') {
|
|
573
|
+
const sig = src.slice(0, init.start)
|
|
574
|
+
const units = init.properties.map((p, i) => ({ key: propKey(p, i), text: src.slice(p.start, p.end) }))
|
|
575
|
+
return { sig, units, open: '{\n ', join: ',\n ', close: '\n}' }
|
|
576
|
+
}
|
|
577
|
+
if ((init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') && init.body && init.body.type === 'BlockStatement') {
|
|
578
|
+
const bs = init.body.start + 1, be = init.body.end - 1
|
|
579
|
+
const ks = keyStatements(init.body.body)
|
|
580
|
+
const units = init.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
|
|
581
|
+
return { sig: src.slice(0, init.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Stable key for an object-literal property, so two agents adding DIFFERENT keys union.
|
|
588
|
+
function propKey(p, i) {
|
|
589
|
+
if (p.type === 'SpreadElement') return 'spread:' + i
|
|
590
|
+
if (p.key && p.key.type === 'Identifier' && !p.computed) return 'prop:' + p.key.name
|
|
591
|
+
if (p.key && p.key.type === 'Literal') return 'prop:' + String(p.key.value)
|
|
592
|
+
return 'prop:' + i
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Does `src` parse either as top-level code OR as a class member? (The inner-merge result
|
|
596
|
+
// for a method is a fragment that's only valid inside a class — this lets us validate it.)
|
|
597
|
+
function parsesUnit(src) {
|
|
598
|
+
if (parses(src)) return true
|
|
599
|
+
try { parse(MEMBER_WRAP + src + '}'); return true } catch { return false }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// TOKEN STREAM: lex `src` into a flat list of { start, end, k } where k is the token's
|
|
603
|
+
// source text -- the atom the engine's token-level inner merge diffs on. This is what lets
|
|
604
|
+
// ICR merge two edits INSIDE one statement (e.g. `foo(1, 2)` -- one side edits `1`, the
|
|
605
|
+
// other `2`) the way a tree-sitter merge does, instead of conflicting on the whole line.
|
|
606
|
+
// Lexing (not parsing) so it also works on fragments (`return x`, a class-member body).
|
|
607
|
+
// Comments are NOT tokens -- they live in the gaps between tokens and are carried verbatim
|
|
608
|
+
// by the base text, so a comment-only edit is a no-op here (the documented comment limit).
|
|
609
|
+
// Returns null if the fragment can't be lexed -> the engine treats it as a conflict.
|
|
610
|
+
function tokenize(src) {
|
|
611
|
+
const out = []
|
|
612
|
+
try {
|
|
613
|
+
const tz = acorn.tokenizer(src, { ecmaVersion: 'latest' })
|
|
614
|
+
for (;;) {
|
|
615
|
+
const t = tz.getToken()
|
|
616
|
+
if (t.type === acorn.tokTypes.eof) break
|
|
617
|
+
out.push({ start: t.start, end: t.end, k: src.slice(t.start, t.end) })
|
|
618
|
+
}
|
|
619
|
+
} catch { return null }
|
|
620
|
+
return out
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// The provider contract every ICR language module implements.
|
|
624
|
+
export const javascript = {
|
|
625
|
+
id: 'javascript',
|
|
626
|
+
exts: ['.js', '.mjs', '.cjs'],
|
|
627
|
+
parses,
|
|
628
|
+
units,
|
|
629
|
+
declaredNames,
|
|
630
|
+
usedIdentifiers, // approximate (kept for reference)
|
|
631
|
+
referencedFreeNames, // scope-aware — what the engine prefers for the dangling check
|
|
632
|
+
declBody,
|
|
633
|
+
isAsyncDecl,
|
|
634
|
+
upgradeCallSitesToAwait,
|
|
635
|
+
syncCallSitesOf,
|
|
636
|
+
renameRefs, // rewrites every matching identifier (kept for reference)
|
|
637
|
+
renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
|
|
638
|
+
splitUnit, // finer granularity: split a function/class/method into keyed inner units
|
|
639
|
+
parsesUnit, // validity oracle that also accepts a class-member fragment
|
|
640
|
+
mergeUnit, // language-specific same-key merge (import specifier union)
|
|
641
|
+
tokenize, // finest granularity: token stream for the token-level inner merge
|
|
642
|
+
}
|