hivecode-mcp 0.11.1 → 0.11.3
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/docs/HIVE_RULES.md +41 -0
- package/docs/MCP.md +77 -0
- package/grm-link.cjs +141 -0
- package/hive-coord.js +29 -2
- package/hive-mailbox.js +173 -0
- package/hive-mcp.js +73 -9
- package/icr.js +6 -0
- package/lang-brace.js +72 -7
- package/lang-json.js +29 -2
- package/lang-python.js +228 -0
- package/lang-yaml.js +38 -6
- package/limits.js +346 -0
- package/package.json +29 -8
- package/sync.js +317 -8
package/lang-brace.js
CHANGED
|
@@ -31,6 +31,10 @@ function scannerAt(src, strings) {
|
|
|
31
31
|
|
|
32
32
|
const isWS = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n'
|
|
33
33
|
|
|
34
|
+
// Tokens that CONTINUE a declaration onto the next line and can never begin a new top-level
|
|
35
|
+
// one. Used by topUnits to avoid cutting a declaration in half at a newline.
|
|
36
|
+
const CONT = /^(?:__attribute__|__asm__|__asm|__declspec|noexcept|throw\s*\(|->)/
|
|
37
|
+
|
|
34
38
|
// Extract top-level units [{start,end}] from src[from..to]. A unit is a maximal
|
|
35
39
|
// declaration/statement at bracket depth 0: either a block (signature + balanced
|
|
36
40
|
// {…}) or a statement ending at ';' or end-of-line.
|
|
@@ -52,7 +56,36 @@ function topUnits(src, strings, from = 0, to = src.length) {
|
|
|
52
56
|
if (CLOSE[c]) { depth--; i++; if (depth <= 0 && sawBrace) { while (i < to && (src[i] === ' ' || src[i] === '\t')) i++; if (src[i] === ';') i++; break } continue }
|
|
53
57
|
if (depth === 0) {
|
|
54
58
|
if (c === ';') { i++; break }
|
|
55
|
-
if (c === '\n' && !sawBrace)
|
|
59
|
+
if (c === '\n' && !sawBrace) {
|
|
60
|
+
// K&R / Allman / kernel style: the body's opening brace sits on the NEXT line, so
|
|
61
|
+
// this newline is INSIDE a declaration, not the end of one. Breaking here split
|
|
62
|
+
// every such function into two units — `stmt:<signature>` and `stmt:{` — with
|
|
63
|
+
// POSITIONAL keys, so any insertion elsewhere shifted the indices and re-paired
|
|
64
|
+
// body #N with signature #N+1. That silently produced code whose function bodies
|
|
65
|
+
// belonged to other functions, while braces stayed balanced so the parse gate
|
|
66
|
+
// passed. Measured at ~34% of real C merges before this fix.
|
|
67
|
+
// Look ahead past whitespace and comments: if a block follows, it belongs to us.
|
|
68
|
+
let k = i + 1
|
|
69
|
+
for (;;) {
|
|
70
|
+
while (k < to && isWS(src[k])) k++
|
|
71
|
+
const s2 = at(k)
|
|
72
|
+
if (s2 > k && (src[k] === '/' || src[k] === '#')) { k = s2; continue } // comment
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
if (k < to && src[k] === '{') { i++; continue } // keep scanning; the block joins this unit
|
|
76
|
+
// A declaration can also continue onto the next line WITHOUT a brace, e.g.
|
|
77
|
+
// void addReplyErrorFormat(client *c, const char *fmt, ...)
|
|
78
|
+
// __attribute__((format(printf, 2, 3)));
|
|
79
|
+
// Splitting there produced an orphan `__attribute__…;` unit whose text is IDENTICAL
|
|
80
|
+
// across many prototypes in a header, so the keys became positional (#2, #3, …) and
|
|
81
|
+
// shifted exactly like the signature/body case — orphaning attributes onto the wrong
|
|
82
|
+
// prototypes. Only tokens that can never BEGIN a new top-level declaration are listed
|
|
83
|
+
// here: `const`/`override`/`final` are deliberately excluded because in TS/Rust they
|
|
84
|
+
// legitimately start the next statement (the C++ uses of those end in `{`, which the
|
|
85
|
+
// brace lookahead above already handles).
|
|
86
|
+
if (k < to && CONT.test(src.slice(k, k + 16))) { i++; continue }
|
|
87
|
+
break
|
|
88
|
+
}
|
|
56
89
|
}
|
|
57
90
|
i++
|
|
58
91
|
}
|
|
@@ -67,15 +100,33 @@ const VARD = /\b(const|let|var|val|static|final)\s+([A-Za-z_$][\w$]*)/
|
|
|
67
100
|
// void main(String[] a)`, `std::string name()`): optional decorators + any number of
|
|
68
101
|
// modifiers + an optional return-type token, then NAME(args){. The captured name is
|
|
69
102
|
// the identifier directly before `(`.
|
|
70
|
-
|
|
103
|
+
// The return-type group ends with `[\s*&]+` rather than `\s+` so a pointer return type whose
|
|
104
|
+
// star is attached to the NAME still parses: `struct sk_buff *alloc_skb(int n)` must key as
|
|
105
|
+
// `alloc_skb`, not fall through to DECL and key as the return type `sk_buff`.
|
|
106
|
+
// The `Class::` group makes C++ out-of-line member definitions keyable — `int Widget::render(…)`
|
|
107
|
+
// keys as `render`. Without it every C++ method definition fell through to an unkeyed `stmt:`.
|
|
108
|
+
const METH = /^(?:@[\w.()]+\s+)*(?:(?:public|private|protected|internal|static|async|final|override|open|pub|suspend|inline|virtual|export|default|abstract|sealed|unsafe|extern|synchronized|native|constexpr)\s+)*(?:(?:function|fn|func|fun)\s+)?(?:[A-Za-z_$][\w$<>\[\].,:&*\s]*?[\s*&]+)?(?:[A-Za-z_$][\w$]*\s*::\s*)*([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\([^;{]*\)(?:\s*(?:const|noexcept|override|final|volatile|mutable|&&?|throw\s*\([^){]*\)))*\s*(?:->[^{]+|:[^={]+)?\s*\{/
|
|
71
109
|
// control-flow headers also look like `name (…) {` — never key them as methods
|
|
72
110
|
const CTRL = new Set(['if', 'for', 'while', 'switch', 'catch', 'do', 'else', 'try', 'return', 'new', 'throw', 'match', 'select', 'when', 'until', 'lock', 'using', 'foreach', 'with'])
|
|
73
111
|
|
|
74
112
|
function keyOf(text) {
|
|
75
113
|
const head = text.replace(/^\s+/, '')
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
114
|
+
// DECL and METH can both match one declaration, and which is RIGHT depends on whether they
|
|
115
|
+
// agree on the name:
|
|
116
|
+
// `function a() {` -> both say "a" -> keep DECL's richer `function:a`
|
|
117
|
+
// `struct sk_buff *alloc_skb(` -> DECL says "sk_buff",
|
|
118
|
+
// METH says "alloc_skb" -> DECL matched the RETURN TYPE,
|
|
119
|
+
// so METH wins and the key names the function.
|
|
120
|
+
// Preferring DECL unconditionally (the original order) meant every pointer-returning C
|
|
121
|
+
// function was keyed by its return type, feeding the rename/dangling logic wrong names.
|
|
122
|
+
const meth = head.match(METH)
|
|
123
|
+
const decl = head.match(DECL)
|
|
124
|
+
if (meth && !CTRL.has(meth[1])) {
|
|
125
|
+
if (decl && decl[2] === meth[1]) return decl[1] + ':' + decl[2]
|
|
126
|
+
return 'method:' + meth[1]
|
|
127
|
+
}
|
|
128
|
+
if (decl) return decl[1] + ':' + decl[2]
|
|
129
|
+
const m = head.match(VARD); if (m) return 'var:' + m[2]
|
|
79
130
|
return 'stmt:' + head.split('\n')[0].trim().replace(/\s+/g, ' ')
|
|
80
131
|
}
|
|
81
132
|
|
|
@@ -130,16 +181,30 @@ function isRefToken(src, t) {
|
|
|
130
181
|
function balanced(src, strings) {
|
|
131
182
|
const at = scannerAt(src, strings)
|
|
132
183
|
const stack = []
|
|
184
|
+
let cpp = 0 // #if/#ifdef/#ifndef nesting depth
|
|
185
|
+
let lineStart = true // only whitespace seen so far on this line
|
|
133
186
|
let i = 0
|
|
134
187
|
while (i < src.length) {
|
|
135
188
|
const s = at(i)
|
|
136
|
-
if (s > i) { if (s > src.length) return false; i = s; continue }
|
|
189
|
+
if (s > i) { if (s > src.length) return false; i = s; lineStart = false; continue }
|
|
137
190
|
const c = src[i]
|
|
138
191
|
if (OPEN[c]) stack.push(c)
|
|
139
192
|
else if (CLOSE[c]) { if (stack.pop() !== CLOSE[c]) return false }
|
|
193
|
+
// PREPROCESSOR NESTING. Brace balance alone cannot see a dropped `#endif`: the merged
|
|
194
|
+
// text stays perfectly brace-balanced while the conditional structure is broken, so the
|
|
195
|
+
// gate passed and invalid C shipped as clean. Directives are matched only at the start
|
|
196
|
+
// of a line, so `#[derive]`, `#region` and `#` inside strings/comments are unaffected
|
|
197
|
+
// (strings and comments are skipped by the scanner above).
|
|
198
|
+
else if (c === '#' && lineStart) {
|
|
199
|
+
const rest = src.slice(i + 1, i + 12).replace(/^\s+/, '')
|
|
200
|
+
if (/^(ifdef|ifndef|if)\b/.test(rest)) cpp++
|
|
201
|
+
else if (/^endif\b/.test(rest)) { cpp--; if (cpp < 0) return false }
|
|
202
|
+
}
|
|
203
|
+
if (c === '\n') lineStart = true
|
|
204
|
+
else if (c !== ' ' && c !== '\t' && c !== '\r') lineStart = false
|
|
140
205
|
i++
|
|
141
206
|
}
|
|
142
|
-
return stack.length === 0
|
|
207
|
+
return stack.length === 0 && cpp === 0
|
|
143
208
|
}
|
|
144
209
|
|
|
145
210
|
export function makeBraceLanguage({ id, exts, strings = ['"', "'", '`'] }) {
|
package/lang-json.js
CHANGED
|
@@ -82,6 +82,34 @@ function mergeArray(base, a, b) {
|
|
|
82
82
|
return out
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// SYMMETRY. The merged object is re-serialized with JSON.stringify, which emits keys in
|
|
86
|
+
// INSERTION order — so the order we walk the key union IS the output byte order. The
|
|
87
|
+
// obvious union, `[...keys(base), ...keys(a), ...keys(b)]`, is side-ordered: ours's new
|
|
88
|
+
// keys always precede theirs's, so merge(base,a,b) and merge(base,b,a) produce clean
|
|
89
|
+
// 'auto' results that differ byte-for-byte. Measured at 43/167 auto merges (26%) by
|
|
90
|
+
// test/icr-data-torture-test.js. Semantically identical, but P2 SYMMETRY is asserted
|
|
91
|
+
// byte-wise for every other language (icr-torture-test.js), and byte-inequality is
|
|
92
|
+
// precisely what a CRDT peer and a git blob hash react to — the two consumers the README
|
|
93
|
+
// names. So the union order must not depend on which side is called `a`:
|
|
94
|
+
// • keys present in base keep BASE's order (unchanged behaviour, minimal diffs);
|
|
95
|
+
// • keys added by either side follow, ordered by where their own author put them
|
|
96
|
+
// (min index across the sides that have it), ties broken by key name.
|
|
97
|
+
// Both rules are symmetric in a/b, so swapping the sides cannot change the output.
|
|
98
|
+
function unionKeys(base, a, b) {
|
|
99
|
+
const rank = new Map()
|
|
100
|
+
for (const obj of [a, b]) {
|
|
101
|
+
const ks = Object.keys(obj)
|
|
102
|
+
for (let i = 0; i < ks.length; i++) {
|
|
103
|
+
const k = ks[i]
|
|
104
|
+
if (k in base) continue
|
|
105
|
+
const prev = rank.get(k)
|
|
106
|
+
if (prev === undefined || i < prev) rank.set(k, i)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const added = [...rank.keys()].sort((x, y) => (rank.get(x) - rank.get(y)) || (x < y ? -1 : x > y ? 1 : 0))
|
|
110
|
+
return [...Object.keys(base), ...added]
|
|
111
|
+
}
|
|
112
|
+
|
|
85
113
|
// Value-wise 3-way merge. Fills `conflicts` (path strings) and `resolvable`
|
|
86
114
|
// ({key, base, ours, theirs} with pretty-printed values) on same-path clashes;
|
|
87
115
|
// the merged value keeps BASE at conflicted paths (the engine refuses the whole
|
|
@@ -92,8 +120,7 @@ function mergeVal(base, a, b, path, conflicts, resolvable) {
|
|
|
92
120
|
if (eq(b, base)) return a
|
|
93
121
|
if (isObj(base) && isObj(a) && isObj(b)) {
|
|
94
122
|
const out = {}
|
|
95
|
-
const
|
|
96
|
-
for (const k of keys) {
|
|
123
|
+
for (const k of unionKeys(base, a, b)) {
|
|
97
124
|
const p = path ? path + '.' + k : k
|
|
98
125
|
const inB = k in base, inA = k in a, inBb = k in b
|
|
99
126
|
if (inB && !inA && !inBb) continue // deleted on both sides
|
package/lang-python.js
CHANGED
|
@@ -264,6 +264,224 @@ function isRefToken(src, t) {
|
|
|
264
264
|
return !(p >= 0 && src[p] === '.')
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// --- scope-aware reference analysis ---------------------------------------------
|
|
268
|
+
//
|
|
269
|
+
// WHY THIS IS EXACT AND NOT ANOTHER HEURISTIC. Python binds by assignment: a name
|
|
270
|
+
// assigned anywhere in a function body is local to that function for the WHOLE body, and
|
|
271
|
+
// reading it before the assignment is an UnboundLocalError rather than a global read. So
|
|
272
|
+
// a function that assigns `helper` cannot also be referring to a module-level `helper` —
|
|
273
|
+
// the language forbids the mixed case. That is what makes subtracting local bindings safe
|
|
274
|
+
// here, where a plain "does this name appear anywhere" check has to stay over-cautious.
|
|
275
|
+
//
|
|
276
|
+
// WHAT IT FIXES. usedIdentifiers reports every name it sees, so deleting a top-level
|
|
277
|
+
// `helper` while some unrelated function happens to have a local named `helper` was
|
|
278
|
+
// reported as ref:helper — a merge refused that was perfectly safe, falling back to line
|
|
279
|
+
// merge with conflict markers. Common names (config, data, result, helper) trip this
|
|
280
|
+
// constantly. Reproduced in test/scope-repro.mjs before this existed.
|
|
281
|
+
//
|
|
282
|
+
// THE DIRECTION THAT MATTERS. Missing a REAL reference would let ICR delete something
|
|
283
|
+
// still in use — the one failure this library promises never to have — so every rule
|
|
284
|
+
// below fails towards "still referenced":
|
|
285
|
+
// • module scope subtracts nothing, so top-level rebinding never hides a reference;
|
|
286
|
+
// • a nested def is its own scope, never folded into its parent's bindings;
|
|
287
|
+
// • a class body contributes no bindings at all (its names are attributes, invisible
|
|
288
|
+
// to methods), so they stay free;
|
|
289
|
+
// • `global`/`nonlocal` remove a name from the local set;
|
|
290
|
+
// • subscript and attribute targets (`a[i] = `, `a.b = `) bind nothing — `a` must
|
|
291
|
+
// already exist, so it stays a reference;
|
|
292
|
+
// • anything unrecognised is left free.
|
|
293
|
+
const PY_KW = new Set(['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
|
|
294
|
+
'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for',
|
|
295
|
+
'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass',
|
|
296
|
+
'raise', 'return', 'try', 'while', 'with', 'yield'])
|
|
297
|
+
|
|
298
|
+
const isName = (k) => /^[A-Za-z_]\w*$/.test(k) && !PY_KW.has(k)
|
|
299
|
+
const OPEN = new Set(['(', '[', '{'])
|
|
300
|
+
const CLOSE = new Set([')', ']', '}'])
|
|
301
|
+
|
|
302
|
+
// A target position binds only a bare name. `a[i] = x` and `a.b = x` require `a` to exist
|
|
303
|
+
// already, so they are reads, not bindings.
|
|
304
|
+
function bareTargets(toks, from, to, out) {
|
|
305
|
+
for (let i = from; i < to; i++) {
|
|
306
|
+
const t = toks[i]
|
|
307
|
+
if (!isName(t.k)) continue
|
|
308
|
+
const prev = toks[i - 1] && toks[i - 1].k
|
|
309
|
+
const next = toks[i + 1] && toks[i + 1].k
|
|
310
|
+
if (prev === '.' || next === '.' || next === '[' || next === '(') continue
|
|
311
|
+
out.add(t.k)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Names this statement binds in its own scope, and names it explicitly disowns.
|
|
316
|
+
function stmtBindings(text) {
|
|
317
|
+
const bind = new Set(), disown = new Set()
|
|
318
|
+
const toks = lexTokens(text).filter((t) => t.k.trim() !== '')
|
|
319
|
+
const kw = toks.length ? toks[0].k : ''
|
|
320
|
+
|
|
321
|
+
if (kw === 'global' || kw === 'nonlocal') {
|
|
322
|
+
for (const t of toks.slice(1)) if (isName(t.k)) disown.add(t.k)
|
|
323
|
+
return { bind, disown }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (kw === 'import' || kw === 'from') {
|
|
327
|
+
// `import a.b as c` binds c; `import a.b` binds a; `from m import x, y as z`.
|
|
328
|
+
const start = kw === 'from' ? toks.findIndex((t) => t.k === 'import') : 0
|
|
329
|
+
if (start >= 0) {
|
|
330
|
+
for (let i = start + 1; i < toks.length; i++) {
|
|
331
|
+
if (toks[i].k === 'as' && isName((toks[i + 1] || {}).k)) { bind.add(toks[i + 1].k); i++; continue }
|
|
332
|
+
// first segment of a dotted path, and only when not renamed by a later `as`
|
|
333
|
+
if (isName(toks[i].k) && (toks[i - 1] || {}).k !== '.' &&
|
|
334
|
+
(toks[i + 1] || {}).k !== '.' && (toks[i + 1] || {}).k !== 'as') bind.add(toks[i].k)
|
|
335
|
+
else if (isName(toks[i].k) && (toks[i + 1] || {}).k === '.' && kw === 'import') bind.add(toks[i].k)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { bind, disown }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let depth = 0
|
|
342
|
+
for (let i = 0; i < toks.length; i++) {
|
|
343
|
+
const k = toks[i].k
|
|
344
|
+
if (OPEN.has(k)) { depth++; continue }
|
|
345
|
+
if (CLOSE.has(k)) { depth--; continue }
|
|
346
|
+
|
|
347
|
+
// `for <targets> in` — at any depth, so comprehensions bind too.
|
|
348
|
+
if (k === 'for') {
|
|
349
|
+
let j = i + 1
|
|
350
|
+
while (j < toks.length && toks[j].k !== 'in') j++
|
|
351
|
+
bareTargets(toks, i + 1, j, bind)
|
|
352
|
+
i = j
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
// `with x as name`, `except E as name`
|
|
356
|
+
if (k === 'as' && isName((toks[i + 1] || {}).k)) { bind.add(toks[i + 1].k); i++; continue }
|
|
357
|
+
|
|
358
|
+
if (k !== '=') continue
|
|
359
|
+
const prev = (toks[i - 1] || {}).k
|
|
360
|
+
const next = (toks[i + 1] || {}).k
|
|
361
|
+
if (next === '=') { i++; continue } // ==
|
|
362
|
+
if (prev === '=' || prev === '!' || prev === '<' || prev === '>') continue
|
|
363
|
+
if (depth > 0) {
|
|
364
|
+
// Inside brackets `=` is a keyword argument, except for the walrus `:=`.
|
|
365
|
+
if (prev === ':' && isName((toks[i - 2] || {}).k)) bind.add(toks[i - 2].k)
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
// Statement-level assignment: simple, augmented (`+=`, prev is an operator), or
|
|
369
|
+
// annotated (`x: int = 1`). Everything left of it is the target list.
|
|
370
|
+
bareTargets(toks, 0, i, bind)
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
return { bind, disown }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function refsIn(text, emit) {
|
|
377
|
+
for (const t of lexTokens(text)) if (isName(t.k) && isRefToken(text, t)) emit(t.k)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parameter names of a def header — used as bindings of the CHILD scope, and excluded
|
|
381
|
+
// from the header's own references so a parameter is never mistaken for a free name.
|
|
382
|
+
function defParams(header) {
|
|
383
|
+
const toks = lexTokens(header).filter((t) => t.k.trim() !== '')
|
|
384
|
+
const open = toks.findIndex((t) => t.k === '(')
|
|
385
|
+
if (open < 0) return new Set()
|
|
386
|
+
const params = new Set()
|
|
387
|
+
let depth = 0
|
|
388
|
+
for (let i = open; i < toks.length; i++) {
|
|
389
|
+
const k = toks[i].k
|
|
390
|
+
if (OPEN.has(k)) { depth++; continue }
|
|
391
|
+
if (CLOSE.has(k)) { depth--; if (depth === 0) break; continue }
|
|
392
|
+
if (depth !== 1 || !isName(k)) continue
|
|
393
|
+
const prev = (toks[i - 1] || {}).k
|
|
394
|
+
if (prev === '(' || prev === ',' || prev === '*' || prev === '**') params.add(k)
|
|
395
|
+
}
|
|
396
|
+
return params
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Split a def/class unit into the part evaluated in the ENCLOSING scope (decorators and
|
|
400
|
+
// the header line — where default values and base classes live) and the body, which is
|
|
401
|
+
// its own scope.
|
|
402
|
+
function splitDeclUnit(text) {
|
|
403
|
+
const L = lines(text)
|
|
404
|
+
let h = 0
|
|
405
|
+
while (h < L.length && (L[h].blank || /^\s*@/.test(L[h].text))) h++
|
|
406
|
+
if (h >= L.length) return null
|
|
407
|
+
const head = L[h]
|
|
408
|
+
const isDef = HEAD.test(head.text), isCls = CLS.test(head.text)
|
|
409
|
+
if (!isDef && !isCls) return null
|
|
410
|
+
// A signature can span lines while its brackets are open.
|
|
411
|
+
let e = h
|
|
412
|
+
let depth = 0
|
|
413
|
+
do {
|
|
414
|
+
for (const ch of L[e].text) { if (OPEN.has(ch)) depth++; else if (CLOSE.has(ch)) depth-- }
|
|
415
|
+
e++
|
|
416
|
+
} while (e < L.length && depth > 0)
|
|
417
|
+
const header = text.slice(0, L[e - 1].end)
|
|
418
|
+
let b = e
|
|
419
|
+
while (b < L.length && L[b].blank) b++
|
|
420
|
+
const body = b < L.length ? text.slice(L[b].start) : ''
|
|
421
|
+
return { isDef, header, body, bodyIndent: b < L.length ? L[b].indent : 0 }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Walk one scope's statements, descending through compound bodies (same scope) but
|
|
425
|
+
// stopping at def/class (new scope).
|
|
426
|
+
function scopeParts(code, base, onStmt, onDecl) {
|
|
427
|
+
for (const u of blocks(code, base)) {
|
|
428
|
+
const text = code.slice(u.start, u.end)
|
|
429
|
+
if (splitDeclUnit(text)) { onDecl(text); continue }
|
|
430
|
+
const L = lines(text)
|
|
431
|
+
let b = 1
|
|
432
|
+
while (b < L.length && L[b].blank) b++
|
|
433
|
+
if (b < L.length && L[b].indent > base) {
|
|
434
|
+
onStmt(text.slice(0, L[b].start)) // the `if ...:` / `for ...:` head
|
|
435
|
+
scopeParts(text.slice(L[b].start), L[b].indent, onStmt, onDecl)
|
|
436
|
+
} else onStmt(text)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// `subtract` is false at module scope: a module-level rebinding must never hide a
|
|
441
|
+
// reference to the declaration being deleted.
|
|
442
|
+
function scopeFree(code, base, enclosing, free, subtract) {
|
|
443
|
+
const stmts = [], decls = []
|
|
444
|
+
scopeParts(code, base, (t) => stmts.push(t), (t) => decls.push(t))
|
|
445
|
+
|
|
446
|
+
const local = new Set(), disowned = new Set()
|
|
447
|
+
for (const t of stmts) {
|
|
448
|
+
const { bind, disown } = stmtBindings(t)
|
|
449
|
+
for (const n of bind) local.add(n)
|
|
450
|
+
for (const n of disown) disowned.add(n)
|
|
451
|
+
}
|
|
452
|
+
for (const t of decls) {
|
|
453
|
+
const s = splitDeclUnit(t)
|
|
454
|
+
const m = s && (s.header.match(HEAD) || s.header.match(CLS))
|
|
455
|
+
if (m) local.add(m[1])
|
|
456
|
+
}
|
|
457
|
+
for (const n of disowned) local.delete(n)
|
|
458
|
+
|
|
459
|
+
const bound = subtract ? new Set([...enclosing, ...local]) : new Set(enclosing)
|
|
460
|
+
for (const t of stmts) refsIn(t, (n) => { if (!bound.has(n)) free.add(n) })
|
|
461
|
+
|
|
462
|
+
for (const t of decls) {
|
|
463
|
+
const s = splitDeclUnit(t)
|
|
464
|
+
if (!s) continue
|
|
465
|
+
const params = s.isDef ? defParams(s.header) : new Set()
|
|
466
|
+
const declName = (s.header.match(HEAD) || s.header.match(CLS) || [])[1]
|
|
467
|
+
// Decorators, annotations and default values run in THIS scope.
|
|
468
|
+
refsIn(s.header, (n) => {
|
|
469
|
+
if (n === declName || params.has(n) || bound.has(n)) return
|
|
470
|
+
free.add(n)
|
|
471
|
+
})
|
|
472
|
+
if (!s.body) continue
|
|
473
|
+
// A class body contributes no bindings to anything nested inside it, so its methods
|
|
474
|
+
// are analysed against the enclosing scope only. `subtract: s.isDef` is the whole of
|
|
475
|
+
// that rule and it is load-bearing: with subtraction left on for a class, `helper = 9`
|
|
476
|
+
// at class level became a binding the methods inherited, and a real `helper()` call
|
|
477
|
+
// inside a method stopped being reported — a merge shipped with a dangling reference,
|
|
478
|
+
// caught by the safety case in test/scope-repro.mjs. Class attributes are reached as
|
|
479
|
+
// `self.helper` or `C.helper`, never as a bare name, so they must never shadow.
|
|
480
|
+
scopeFree(s.body, s.bodyIndent,
|
|
481
|
+
s.isDef ? new Set([...bound, ...params]) : bound, free, s.isDef)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
267
485
|
// PER-STATEMENT bracket nesting — the soundness backstop the whole-file `balanced()`
|
|
268
486
|
// (global) and `indentsOk()` (indentation) both miss. A token merge can splice a result
|
|
269
487
|
// that is GLOBALLY balanced yet LOCALLY mis-nested: side A reformats `f(x)` to span lines
|
|
@@ -308,6 +526,16 @@ export const python = {
|
|
|
308
526
|
for (const t of lexTokens(src)) if (isRefToken(src, t)) used.add(t.k)
|
|
309
527
|
return used
|
|
310
528
|
},
|
|
529
|
+
// Scope-aware upgrade of the above — the engine prefers this for the dangling-reference
|
|
530
|
+
// check (icr.js). Names that resolve to a LOCAL binding are not reported, so a deleted
|
|
531
|
+
// top-level `helper` is no longer held hostage by an unrelated local of the same name.
|
|
532
|
+
// Kept alongside usedIdentifiers rather than replacing it: the approximate version is
|
|
533
|
+
// still the honest answer for anything this cannot resolve.
|
|
534
|
+
referencedFreeNames: (src) => {
|
|
535
|
+
const free = new Set()
|
|
536
|
+
scopeFree(String(src), 0, new Set(), free, false)
|
|
537
|
+
return free
|
|
538
|
+
},
|
|
311
539
|
// The def/class text with its own name excluded, so renamed twins compare equal.
|
|
312
540
|
// trimEnd(): a unit owns its trailing blank lines, and splicing can change how many
|
|
313
541
|
// survive — trailing whitespace must not defeat the rename match.
|
package/lang-yaml.js
CHANGED
|
@@ -39,13 +39,44 @@ function lines(src) {
|
|
|
39
39
|
return out
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// KEY SHAPES. A YAML key may be QUOTED, and a quoted key legally contains ':' and '#';
|
|
43
|
+
// a plain key may contain '#' too ('#' only opens a comment at line start or after a
|
|
44
|
+
// space). The original class excluded both characters from the key body, so any such key
|
|
45
|
+
// failed KEY and fell through to `'stmt:' + <whole head line>` — a unit key that contains
|
|
46
|
+
// the VALUE. Two sides editing that ONE key then produced two DIFFERENT unit keys, so the
|
|
47
|
+
// engine saw unrelated add/deletes, unioned them, and emitted the key TWICE while
|
|
48
|
+
// reporting the merge 'auto' and clean. Duplicate mapping keys are invalid YAML and every
|
|
49
|
+
// loader keeps the last, so one side's edit was silently discarded — no-silent-loss
|
|
50
|
+
// failing exactly as it did for type-first method signatures in lang-brace.js.
|
|
51
|
+
// Measured before the fix (both top level and nested): "a:b", 'a:b', foo#bar, "a#b" all
|
|
52
|
+
// auto-merged into a duplicated key; plain keys conflicted correctly.
|
|
53
|
+
const QKEY = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*')\s*:(\s|$)/ // "quoted key": / 'quoted key':
|
|
54
|
+
const KEY = /^([^:#\s][^:]*?):(\s|$)/ // `key:` or `key: value` (body may contain '#')
|
|
43
55
|
const DASH = /^-(\s|$)/ // list item
|
|
44
56
|
|
|
57
|
+
// Cut a trailing ` #comment` that is not inside quotes, so it never lands in a key.
|
|
58
|
+
function stripComment(line) {
|
|
59
|
+
const at = scannerAt(line)
|
|
60
|
+
let i = 0
|
|
61
|
+
while (i < line.length) {
|
|
62
|
+
if (line[i] === '#' && i > 0 && /\s/.test(line[i - 1])) return line.slice(0, i)
|
|
63
|
+
const j = at(i)
|
|
64
|
+
if (j > i && line[i] !== '#') { i = j; continue }
|
|
65
|
+
i++
|
|
66
|
+
}
|
|
67
|
+
return line
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// `"a"` and `a` name the SAME key in YAML, so they must key the same — otherwise the
|
|
71
|
+
// pair is the very ambiguity this function exists to remove.
|
|
72
|
+
const unquote = (k) => (/^"(?:[^"\\]|\\.)*"$/.test(k) || /^'(?:[^']|'')*'$/.test(k)) ? k.slice(1, -1) : k
|
|
73
|
+
|
|
45
74
|
function keyOf(text) {
|
|
46
|
-
const t = text.trim()
|
|
47
|
-
|
|
48
|
-
if (m) return 'key:' + m[1]
|
|
75
|
+
const t = stripComment(text.trim()).trim()
|
|
76
|
+
let m = t.match(QKEY)
|
|
77
|
+
if (m) return 'key:' + unquote(m[1])
|
|
78
|
+
m = t.match(KEY)
|
|
79
|
+
if (m) return 'key:' + unquote(m[1].trim())
|
|
49
80
|
if (t === '---' || t === '...') return 'doc:' + t
|
|
50
81
|
return 'stmt:' + t.replace(/\s+/g, ' ')
|
|
51
82
|
}
|
|
@@ -79,10 +110,11 @@ function keyed(raw, src) {
|
|
|
79
110
|
const text = src.slice(u.start, u.end)
|
|
80
111
|
// key by the HEAD line (skipping attached leading comments)
|
|
81
112
|
const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
|
|
82
|
-
|
|
113
|
+
const baseKey = keyOf(head)
|
|
114
|
+
let k = baseKey
|
|
83
115
|
const c = (seen.get(k) || 0) + 1; seen.set(k, c)
|
|
84
116
|
if (c > 1) k += '#' + c
|
|
85
|
-
out.push({ key: k, text, start: u.start, end: u.end })
|
|
117
|
+
out.push({ key: k, baseKey, text, start: u.start, end: u.end })
|
|
86
118
|
}
|
|
87
119
|
return out
|
|
88
120
|
}
|