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/lang-js.js CHANGED
@@ -1,418 +1,642 @@
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 parse(src) {
18
- try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
19
- catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
20
- }
21
-
22
- function parses(src) { try { parse(src); return true } catch { return false } }
23
-
24
- // A stable key per top-level node — what lets us recognize "the same declaration"
25
- // across two edits. Names where we have them; position as a last resort.
26
- function keyOf(node, i) {
27
- let n = node
28
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
29
- if (n.type === 'ExportDefaultDeclaration') return 'export:default'
30
- if (n.type === 'FunctionDeclaration' && n.id) return 'fn:' + n.id.name
31
- if (n.type === 'ClassDeclaration' && n.id) return 'class:' + n.id.name
32
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name)
33
- return 'var:' + n.declarations[0].id.name
34
- // Key imports by their SOURCE MODULE, so two agents adding imports from DIFFERENT
35
- // modules never collide, and two touching the SAME module are merged (specifier union).
36
- if (n.type === 'ImportDeclaration') return 'import:' + (n.source && n.source.value != null ? n.source.value : i)
37
- return 'stmt:' + i
38
- }
39
-
40
- // Top-level units: ordered { key, text, start, end } for each declaration/statement.
41
- // start/end are byte offsets in `src` — used by the format-preserving splice so unchanged
42
- // regions (and the whitespace/comments between units) survive a merge verbatim.
43
- function units(src) {
44
- const ast = parse(src)
45
- return ast.body.map((node, i) => ({ key: keyOf(node, i), text: src.slice(node.start, node.end), start: node.start, end: node.end }))
46
- }
47
-
48
- // Bare declared name (foo, Bar, VERSION) of a node, or null for anonymous statements.
49
- function bareName(node) {
50
- let n = node
51
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
52
- if (n.type === 'FunctionDeclaration' && n.id) return n.id.name
53
- if (n.type === 'ClassDeclaration' && n.id) return n.id.name
54
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name) return n.declarations[0].id.name
55
- return null
56
- }
57
-
58
- function declaredNames(src) {
59
- const s = new Set()
60
- for (const node of parse(src).body) {
61
- const nm = bareName(node); if (nm) s.add(nm)
62
- // Import locals count as declared names too — so if an import is ever dropped and its
63
- // binding is still used, the dangling-reference check catches it (safety net for the
64
- // import merge). Keeps the never-emit-broken-code guarantee honest for imports.
65
- if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
66
- }
67
- return s
68
- }
69
-
70
- // --- import-aware merging -------------------------------------------------------
71
- // Parse a single import statement into its parts, or null if it isn't one.
72
- function parseImport(src) {
73
- let n
74
- try { n = parse(src).body[0] } catch { return null }
75
- if (!n || n.type !== 'ImportDeclaration') return null
76
- let def = null, ns = null
77
- const named = []
78
- for (const sp of n.specifiers) {
79
- if (sp.type === 'ImportDefaultSpecifier') def = sp.local.name
80
- else if (sp.type === 'ImportNamespaceSpecifier') ns = sp.local.name
81
- else named.push({ imported: sp.imported.name != null ? sp.imported.name : sp.imported.value, local: sp.local.name })
82
- }
83
- return { source: n.source.value, def, ns, named }
84
- }
85
- function renderImport({ source, def, ns, named }) {
86
- const parts = []
87
- if (def) parts.push(def)
88
- if (ns) parts.push('* as ' + ns)
89
- if (named.length) parts.push('{ ' + named.map((s) => s.imported === s.local ? s.local : s.imported + ' as ' + s.local).join(', ') + ' }')
90
- if (!parts.length) return "import '" + source + "'"
91
- return 'import ' + parts.join(', ') + " from '" + source + "'"
92
- }
93
- // Both sides changed an import from the SAME module → union their specifiers into one
94
- // statement (the common "both agents added an import" case). Returns null — meaning
95
- // "let the normal conflict path handle it" — for anything we can't safely combine
96
- // (different modules, clashing defaults/namespaces, namespace-mixed-with-named).
97
- function mergeUnit(baseText, aText, bText) {
98
- const a = parseImport(aText), b = parseImport(bText)
99
- if (!a || !b || a.source !== b.source) return null
100
- let def
101
- if (a.def && b.def) { if (a.def !== b.def) return null; def = a.def } else def = a.def || b.def
102
- let ns
103
- if (a.ns && b.ns) { if (a.ns !== b.ns) return null; ns = a.ns } else ns = a.ns || b.ns
104
- if (ns && (a.named.length || b.named.length)) return null // `* as X` can't share a statement with named imports
105
- const seen = new Set(), named = []
106
- for (const s of [...a.named, ...b.named]) { const k = s.imported + '|' + s.local; if (!seen.has(k)) { seen.add(k); named.push(s) } }
107
- return renderImport({ source: a.source, def, ns, named })
108
- }
109
-
110
- // The BODY of a top-level declaration, by name, with the name itself excluded — so two
111
- // declarations that differ only in their name compare equal (that's how we spot a rename).
112
- function declBody(src, name) {
113
- for (const node of parse(src).body) {
114
- if (bareName(node) !== name) continue
115
- let n = node
116
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
117
- if ((n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') && n.body)
118
- return src.slice(n.body.start, n.body.end)
119
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
120
- const init = n.declarations[0].init
121
- return src.slice(init.start, init.end)
122
- }
123
- return null
124
- }
125
- return null
126
- }
127
-
128
- // Rewrite every reference to `oldName` as `newName` (skipping property/key positions,
129
- // which aren't references to the declaration). Edits back-to-front to keep offsets valid.
130
- function renameRefs(src, oldName, newName) {
131
- const spots = []
132
- walk(parse(src), (node, parent, key) => {
133
- if (node.type !== 'Identifier' || node.name !== oldName) return
134
- if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
135
- if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
136
- spots.push([node.start, node.end])
137
- })
138
- spots.sort((a, b) => b[0] - a[0])
139
- let out = src
140
- for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
141
- return out
142
- }
143
-
144
- // Every identifier USED anywhere (skips obj.prop names and non-computed object keys).
145
- // Approximate — good enough for the intent check; the real version is scope-aware.
146
- function usedIdentifiers(src) {
147
- const used = new Set()
148
- walk(parse(src), (node, parent, key) => {
149
- if (node.type !== 'Identifier') return
150
- if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
151
- if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
152
- used.add(node.name)
153
- })
154
- return used
155
- }
156
-
157
- function walk(node, visit, parent = null, key = null) {
158
- if (!node || typeof node.type !== 'string') return
159
- visit(node, parent, key)
160
- for (const k of Object.keys(node)) {
161
- if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') continue
162
- const v = node[k]
163
- if (Array.isArray(v)) { for (const c of v) walk(c, visit, node, k) }
164
- else if (v && typeof v.type === 'string') walk(v, visit, node, k)
165
- }
166
- }
167
-
168
- // --- scope-aware reference analysis ---------------------------------------------
169
- // The names REFERENCED in `src` that resolve to NOTHING in the file's own scopes —
170
- // i.e. free/global names, including any use of a top-level declaration. This is the
171
- // scope-aware upgrade of usedIdentifiers: a use of `x` that resolves to a LOCAL binding
172
- // (a param, a `const x` in the same function, a catch var, a loop var…) is NOT counted,
173
- // because it isn't a reference to a top-level declaration. That lets ICR tell a deleted
174
- // top-level `helper` apart from an unrelated local `helper` that merely shares its name.
175
- //
176
- // Honest scope coverage: functions (declaration/expression/arrow), blocks, catch
177
- // clauses, for/for-in/for-of loop bindings, params with simple destructuring, imports,
178
- // var-hoisting to the function scope, and let/const/class/function block binding.
179
- // Approximations (rare, and they only ever make the dangling check MORE cautious):
180
- // default-value expressions in patterns and `with`/eval are not modeled.
181
- // Walk the AST and call emit(identifierNode) for every reference that resolves to NO
182
- // local binding (free / would resolve to module or global scope). Both the dangling-ref
183
- // check and scope-aware renaming are built on this single traversal.
184
- function walkFreeRefs(src, emit) { resolveScopes(parse(src), null, emit) }
185
-
186
- function referencedFreeNames(src) {
187
- const free = new Set()
188
- walkFreeRefs(src, (node) => free.add(node.name))
189
- return free
190
- }
191
-
192
- // Rename ONLY the free references to oldName (those that resolve to the top-level/global
193
- // binding) — never a local variable that merely shares the name. Edits back-to-front.
194
- function renameFreeRefs(src, oldName, newName) {
195
- const spots = []
196
- walkFreeRefs(src, (node) => { if (node.name === oldName) spots.push([node.start, node.end]) })
197
- spots.sort((a, b) => b[0] - a[0])
198
- let out = src
199
- for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
200
- return out
201
- }
202
-
203
- const CHILD_SKIP = new Set(['type', 'start', 'end', 'loc', 'range'])
204
- function eachChild(node, fn) {
205
- for (const k of Object.keys(node)) {
206
- if (CHILD_SKIP.has(k)) continue
207
- const v = node[k]
208
- if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') fn(c) }
209
- else if (v && typeof v.type === 'string') fn(v)
210
- }
211
- }
212
-
213
- // Names introduced by a binding target (handles identifiers + simple destructuring).
214
- function bindingNames(node, out) {
215
- if (!node) return
216
- switch (node.type) {
217
- case 'Identifier': out.add(node.name); break
218
- case 'ObjectPattern': for (const p of node.properties) bindingNames(p.type === 'RestElement' ? p.argument : p.value, out); break
219
- case 'ArrayPattern': for (const el of node.elements) if (el) bindingNames(el, out); break
220
- case 'AssignmentPattern': bindingNames(node.left, out); break
221
- case 'RestElement': bindingNames(node.argument, out); break
222
- }
223
- }
224
-
225
- // A binding target's NAMES are bindings, but its default values (`a = expr`) and computed
226
- // destructuring keys (`{ [k]: v }`) are REFERENCES that must be resolved against the scope.
227
- // Without this, a name used only in a default value would be missed by the dangling check.
228
- function resolveBindingDefaults(node, scope, emit) {
229
- if (!node) return
230
- switch (node.type) {
231
- case 'AssignmentPattern': resolveBindingDefaults(node.left, scope, emit); resolveScopes(node.right, scope, emit); break
232
- case 'ObjectPattern': for (const p of node.properties) {
233
- if (p.type === 'RestElement') { resolveBindingDefaults(p.argument, scope, emit); break }
234
- if (p.computed) resolveScopes(p.key, scope, emit)
235
- resolveBindingDefaults(p.value, scope, emit)
236
- } break
237
- case 'ArrayPattern': for (const el of node.elements) if (el) resolveBindingDefaults(el, scope, emit); break
238
- case 'RestElement': resolveBindingDefaults(node.argument, scope, emit); break
239
- // Identifier: a pure binding — nothing to resolve.
240
- }
241
- }
242
-
243
- const makeScope = (parent, fnScope) => { const s = { vars: new Set(), parent }; s.fnScope = fnScope || s; return s }
244
- const resolves = (scope, name) => { for (let s = scope; s; s = s.parent) if (s.vars.has(name)) return true; return false }
245
-
246
- // Hoist var declarations + function declarations into the function scope (deep, but not
247
- // crossing into nested functions, which own their own var scope).
248
- function hoistFunctionScope(nodes, scope) { for (const n of nodes) collectHoisted(n, scope) }
249
- function collectHoisted(node, scope) {
250
- if (!node || typeof node.type !== 'string') return
251
- if (node.type === 'FunctionDeclaration') { if (node.id) scope.fnScope.vars.add(node.id.name); return }
252
- if (/Function(Expression)?$|ArrowFunctionExpression/.test(node.type)) return // nested function: stop
253
- if (node.type === 'VariableDeclaration' && node.kind === 'var')
254
- for (const d of node.declarations) bindingNames(d.id, scope.fnScope.vars)
255
- eachChild(node, (c) => collectHoisted(c, scope))
256
- }
257
- // Bind let/const/class/function/import names declared directly in a block/program.
258
- function hoistBlock(nodes, scope) {
259
- for (const n of nodes) {
260
- if (n.type === 'VariableDeclaration' && (n.kind === 'let' || n.kind === 'const'))
261
- for (const d of n.declarations) bindingNames(d.id, scope.vars)
262
- else if (n.type === 'ClassDeclaration' && n.id) scope.vars.add(n.id.name)
263
- else if (n.type === 'FunctionDeclaration' && n.id) scope.vars.add(n.id.name)
264
- else if (n.type === 'ImportDeclaration') for (const sp of n.specifiers) scope.vars.add(sp.local.name)
265
- }
266
- }
267
-
268
- function resolveScopes(node, scope, emit) {
269
- if (!node || typeof node.type !== 'string') return
270
- switch (node.type) {
271
- case 'Program': {
272
- const s = makeScope(null, null)
273
- hoistFunctionScope(node.body, s); hoistBlock(node.body, s)
274
- for (const c of node.body) resolveScopes(c, s, emit)
275
- return
276
- }
277
- case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': {
278
- const s = makeScope(scope, null)
279
- for (const p of node.params) bindingNames(p, s.vars)
280
- for (const p of node.params) resolveBindingDefaults(p, s, emit) // default values are references
281
- if (node.id && node.type === 'FunctionExpression') s.vars.add(node.id.name) // named fn expr
282
- if (node.body.type === 'BlockStatement') {
283
- hoistFunctionScope(node.body.body, s); hoistBlock(node.body.body, s)
284
- for (const c of node.body.body) resolveScopes(c, s, emit)
285
- } else resolveScopes(node.body, s, emit) // arrow with expression body
286
- return
287
- }
288
- case 'BlockStatement': {
289
- const s = makeScope(scope, scope.fnScope); hoistBlock(node.body, s)
290
- for (const c of node.body) resolveScopes(c, s, emit)
291
- return
292
- }
293
- case 'CatchClause': {
294
- const s = makeScope(scope, scope.fnScope)
295
- if (node.param) bindingNames(node.param, s.vars)
296
- resolveScopes(node.body, s, emit)
297
- return
298
- }
299
- case 'ForStatement': {
300
- const s = makeScope(scope, scope.fnScope)
301
- if (node.init && node.init.type === 'VariableDeclaration') hoistBlock([node.init], s)
302
- for (const k of ['init', 'test', 'update', 'body']) if (node[k]) resolveScopes(node[k], s, emit)
303
- return
304
- }
305
- case 'ForInStatement': case 'ForOfStatement': {
306
- const s = makeScope(scope, scope.fnScope)
307
- if (node.left.type === 'VariableDeclaration') hoistBlock([node.left], s)
308
- else resolveScopes(node.left, s, emit)
309
- resolveScopes(node.right, s, emit); resolveScopes(node.body, s, emit)
310
- return
311
- }
312
- case 'VariableDeclarator': { resolveBindingDefaults(node.id, scope, emit); if (node.init) resolveScopes(node.init, scope, emit); return } // id is a binding (defaults are refs)
313
- case 'Identifier': { if (!resolves(scope, node.name)) emit(node); return }
314
- case 'MemberExpression': { resolveScopes(node.object, scope, emit); if (node.computed) resolveScopes(node.property, scope, emit); return }
315
- case 'Property': { if (node.computed) resolveScopes(node.key, scope, emit); resolveScopes(node.value, scope, emit); return }
316
- case 'PropertyDefinition': case 'MethodDefinition': { if (node.computed) resolveScopes(node.key, scope, emit); if (node.value) resolveScopes(node.value, scope, emit); return }
317
- case 'LabeledStatement': { resolveScopes(node.body, scope, emit); return }
318
- case 'BreakStatement': case 'ContinueStatement': return // labels aren't value references
319
- case 'ExportSpecifier': { resolveScopes(node.local, scope, emit); return }
320
- case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': return // binding positions
321
- default: eachChild(node, (c) => resolveScopes(c, scope, emit))
322
- }
323
- }
324
-
325
- // A stable key for a class member, so two agents editing DIFFERENT methods of the same
326
- // class merge, while editing the SAME method conflicts.
327
- function memberKey(m, i) {
328
- const kind = m.type === 'MethodDefinition' ? (m.kind === 'constructor' ? 'ctor' : 'method') : 'field'
329
- if (m.key && m.key.type === 'Identifier' && !m.computed)
330
- return kind + ':' + (m.static ? 'static.' : '') + m.key.name
331
- return 'member:' + i
332
- }
333
-
334
- // For finer-grained merging: split a single declaration into its signature text + keyed
335
- // inner units, plus how to reassemble them. Handles FUNCTIONS (body statements, keyed
336
- // like top-level decls) and CLASSES (members, keyed by method/field name). Returns null
337
- // for anything else (the engine then treats the declaration as indivisible). Tolerates
338
- // fragments that don't parse standalone (e.g. a bare `return`).
339
- const MEMBER_WRAP = 'class __ICR__{'
340
- // A class member (method) doesn't parse standalone — wrap it in a throwaway class so we
341
- // can recurse INTO a method body that both agents edited. Offsets are mapped back to src.
342
- function splitMember(src) {
343
- let ast
344
- try { ast = parse(MEMBER_WRAP + src + '}') } catch { return null }
345
- const cls = ast.body[0]
346
- if (!cls || cls.type !== 'ClassDeclaration' || !cls.body.body.length) return null
347
- const m = cls.body.body[0]
348
- if (!m || m.type !== 'MethodDefinition' || !m.value || !m.value.body || m.value.body.type !== 'BlockStatement') return null
349
- const off = MEMBER_WRAP.length, body = m.value.body
350
- const sig = src.slice(0, body.start - off)
351
- const units = body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start - off, s.end - off) }))
352
- return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
353
- }
354
-
355
- function splitUnit(src) {
356
- let ast
357
- try { ast = parse(src) } catch { return splitMember(src) } // maybe a class-member fragment
358
- let n = ast.body[0]
359
- if (!n) return null
360
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
361
- if (n.type === 'FunctionDeclaration' && n.body) {
362
- const sig = src.slice(n.start, n.body.start)
363
- const units = n.body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start, s.end) }))
364
- return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
365
- }
366
- if (n.type === 'ClassDeclaration' && n.body) {
367
- const sig = src.slice(n.start, n.body.start)
368
- const units = n.body.body.map((m, i) => ({ key: memberKey(m, i), text: src.slice(m.start, m.end) }))
369
- return { sig, units, open: '{\n ', join: '\n\n ', close: '\n}' }
370
- }
371
- // `const x = { ... }` / `const x = () => { ... }` / `const x = function () { ... }`
372
- if (n.type === 'VariableDeclaration' && n.declarations.length === 1 && n.declarations[0].init) {
373
- const init = n.declarations[0].init
374
- if (init.type === 'ObjectExpression') {
375
- const sig = src.slice(n.start, init.start)
376
- const units = init.properties.map((p, i) => ({ key: propKey(p, i), text: src.slice(p.start, p.end) }))
377
- return { sig, units, open: '{\n ', join: ',\n ', close: '\n}' }
378
- }
379
- if ((init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') && init.body && init.body.type === 'BlockStatement') {
380
- const sig = src.slice(n.start, init.body.start)
381
- const units = init.body.body.map((s, i) => ({ key: keyOf(s, i), text: src.slice(s.start, s.end) }))
382
- return { sig, units, open: '{\n ', join: '\n ', close: '\n}' }
383
- }
384
- }
385
- return null
386
- }
387
-
388
- // Stable key for an object-literal property, so two agents adding DIFFERENT keys union.
389
- function propKey(p, i) {
390
- if (p.type === 'SpreadElement') return 'spread:' + i
391
- if (p.key && p.key.type === 'Identifier' && !p.computed) return 'prop:' + p.key.name
392
- if (p.key && p.key.type === 'Literal') return 'prop:' + String(p.key.value)
393
- return 'prop:' + i
394
- }
395
-
396
- // Does `src` parse either as top-level code OR as a class member? (The inner-merge result
397
- // for a method is a fragment that's only valid inside a class — this lets us validate it.)
398
- function parsesUnit(src) {
399
- if (parses(src)) return true
400
- try { parse(MEMBER_WRAP + src + '}'); return true } catch { return false }
401
- }
402
-
403
- // The provider contract every ICR language module implements.
404
- export const javascript = {
405
- id: 'javascript',
406
- exts: ['.js', '.mjs', '.cjs'],
407
- parses,
408
- units,
409
- declaredNames,
410
- usedIdentifiers, // approximate (kept for reference)
411
- referencedFreeNames, // scope-aware — what the engine prefers for the dangling check
412
- declBody,
413
- renameRefs, // rewrites every matching identifier (kept for reference)
414
- renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
415
- splitUnit, // finer granularity: split a function/class/method into keyed inner units
416
- parsesUnit, // validity oracle that also accepts a class-member fragment
417
- mergeUnit, // language-specific same-key merge (import specifier union)
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
+ }