gitnexus 1.6.13-rc.20 → 1.6.13-rc.21

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.
@@ -22,16 +22,16 @@
22
22
  * like the Go implementation — `indexes.bindings` is frozen post-
23
23
  * finalize and must not be mutated.
24
24
  */
25
+ import { isClassLike } from '../../scope-resolution/scope/walkers.js';
25
26
  import { coerceSwiftTargets, groupSwiftFilesBySpmTarget } from './target-grouping.js';
26
27
  export function populateSwiftTargetSiblings(parsedFiles, indexes, ctx) {
27
- // Group files by SPM target subtree (the module). No-source-dir → all
28
- // files in one `__default__` bucket.
29
28
  const targets = coerceSwiftTargets(ctx.resolutionConfig);
30
29
  const filesByTarget = groupSwiftFilesBySpmTarget(parsedFiles, (parsed) => parsed.filePath, targets);
31
30
  const augmentations = indexes.bindingAugmentations;
32
31
  for (const [, group] of filesByTarget) {
32
+ populateNestedTypeFragments(group, indexes, augmentations, ctx.fileContents);
33
33
  if (group.length < 2)
34
- continue; // no siblings to share
34
+ continue; // no file siblings to share
35
35
  const siblings = group.map((parsed) => ({
36
36
  filePath: parsed.filePath,
37
37
  defs: [...parsed.localDefs],
@@ -44,18 +44,298 @@ export function populateSwiftTargetSiblings(parsedFiles, indexes, ctx) {
44
44
  if (receiverModule === undefined)
45
45
  continue;
46
46
  for (const def of target.defs) {
47
- const name = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
48
- if (name === '')
49
- continue;
50
- const bucket = getAugmentationBucket(augmentations, receiverModule, name);
51
- if (bucket.some((b) => b.def.nodeId === def.nodeId))
52
- continue;
53
- bucket.push({ def, origin: 'namespace' });
47
+ addNamespaceBinding(augmentations, receiverModule, def);
54
48
  }
55
49
  }
56
50
  }
57
51
  }
58
52
  }
53
+ /**
54
+ * A Swift extension is a second lexical fragment of its extended type. Make
55
+ * nested types declared by the primary fragment visible from every same-target
56
+ * fragment with the same logical owner. Keeping this on class scopes preserves
57
+ * lexical precedence when an unrelated top-level type has the same simple name.
58
+ */
59
+ function populateNestedTypeFragments(group, indexes, augmentations, fileContents) {
60
+ const scopesByOwner = new Map();
61
+ const lineStartsByFile = new Map();
62
+ for (const parsed of group) {
63
+ const source = fileContents.get(parsed.filePath);
64
+ let lineStarts;
65
+ if (source !== undefined) {
66
+ lineStarts = lineStartsByFile.get(parsed.filePath);
67
+ if (lineStarts === undefined) {
68
+ lineStarts = lineStartsOf(source);
69
+ lineStartsByFile.set(parsed.filePath, lineStarts);
70
+ }
71
+ }
72
+ for (const scope of parsed.scopes) {
73
+ if (scope.kind !== 'Class')
74
+ continue;
75
+ const key = scopeOwnerKey(scope, source, lineStarts);
76
+ if (key === undefined)
77
+ continue;
78
+ let scopes = scopesByOwner.get(key);
79
+ if (scopes === undefined) {
80
+ scopes = [];
81
+ scopesByOwner.set(key, scopes);
82
+ }
83
+ scopes.push(scope.id);
84
+ }
85
+ }
86
+ for (const parsed of group) {
87
+ for (const def of parsed.localDefs) {
88
+ if (!isClassLike(def.type) || def.ownerId === undefined)
89
+ continue;
90
+ const owner = indexes.defs.byId.get(def.ownerId);
91
+ if (owner === undefined)
92
+ continue;
93
+ const targetScopes = scopesByOwner.get(logicalOwnerKey(owner));
94
+ if (targetScopes === undefined)
95
+ continue;
96
+ for (const scopeId of targetScopes) {
97
+ addNamespaceBinding(augmentations, scopeId, def);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ function scopeOwnerKey(scope, source, lineStarts) {
103
+ const owner = scope.ownedDefs.find((def) => isClassLike(def.type));
104
+ if (owner !== undefined)
105
+ return logicalOwnerKey(owner);
106
+ // Extension scopes carry no synthetic class def. Capture generation keeps
107
+ // only the trailing owner on members (`Inner.f` for `extension Outer.Inner`),
108
+ // so recover the full owner from this scope's declaration text first.
109
+ if (source !== undefined) {
110
+ const sourceOwner = swiftExtensionOwner(source, scope, lineStarts);
111
+ // Do not last-dot-guess: member qualified names are trailing-only, so
112
+ // `Inner.make` would key `Inner` instead of `Outer.Inner`.
113
+ if (sourceOwner === undefined)
114
+ return undefined;
115
+ const representative = firstBoundDefinition(scope);
116
+ return logicalOwnerKey({
117
+ ...(representative ?? {
118
+ nodeId: sourceOwner,
119
+ filePath: scope.filePath,
120
+ type: 'Class',
121
+ qualifiedName: sourceOwner,
122
+ }),
123
+ qualifiedName: sourceOwner,
124
+ });
125
+ }
126
+ // Hand-built fixtures and old cached shapes may have no source text. Keep
127
+ // the conservative member-prefix fallback, rejecting inconsistent owners.
128
+ let inferredOwner;
129
+ for (const refs of scope.bindings.values()) {
130
+ for (const { def } of refs) {
131
+ const qualifiedName = def.qualifiedName;
132
+ if (qualifiedName === undefined)
133
+ continue;
134
+ const separator = qualifiedName.lastIndexOf('.');
135
+ if (separator <= 0)
136
+ continue;
137
+ const candidate = logicalOwnerKey({
138
+ ...def,
139
+ qualifiedName: qualifiedName.slice(0, separator),
140
+ });
141
+ if (inferredOwner !== undefined && inferredOwner !== candidate)
142
+ return undefined;
143
+ inferredOwner = candidate;
144
+ }
145
+ }
146
+ return inferredOwner;
147
+ }
148
+ function firstBoundDefinition(scope) {
149
+ for (const refs of scope.bindings.values()) {
150
+ const first = refs[0]?.def;
151
+ if (first !== undefined)
152
+ return first;
153
+ }
154
+ return undefined;
155
+ }
156
+ /**
157
+ * Read `extension Outer.Inner` from the class-scope source range.
158
+ * Access modifiers and attributes (`public`, `@MainActor`, `@available`)
159
+ * may precede the keyword, so the match is not start-anchored. Attribute
160
+ * message strings and comments must not supply a false `extension Type`.
161
+ * Owner segments use Unicode identifier characters so `Café.Container`
162
+ * is not truncated to `Caf`.
163
+ */
164
+ const SWIFT_TYPE_IDENT = String.raw `[\p{ID_Start}_][\p{ID_Continue}]*`;
165
+ const EXTENSION_OWNER = new RegExp(String.raw `\bextension\s+(${SWIFT_TYPE_IDENT}(?:\s*\.\s*${SWIFT_TYPE_IDENT})*)`, 'u');
166
+ function swiftExtensionOwner(source, scope, lineStarts) {
167
+ const declaration = sliceScopeRange(source, scope.range, lineStarts ?? lineStartsOf(source));
168
+ if (declaration === undefined)
169
+ return undefined;
170
+ const cleaned = cleanExtensionHeader(declaration);
171
+ return EXTENSION_OWNER.exec(cleaned)?.[1]?.replace(/\s+/g, '');
172
+ }
173
+ /** `Scope.range` is 1-based on lines. Columns are Tree-sitter UTF-8 bytes. */
174
+ function lineStartsOf(source) {
175
+ const starts = [0, 0];
176
+ for (let index = 0; index < source.length; index += 1) {
177
+ if (source[index] === '\n')
178
+ starts.push(index + 1);
179
+ }
180
+ return starts;
181
+ }
182
+ function sliceScopeRange(source, range, starts) {
183
+ const start = starts[range.startLine];
184
+ const end = starts[range.endLine];
185
+ if (start === undefined || end === undefined)
186
+ return undefined;
187
+ return source.slice(start + jsOffsetForUtf8Column(source, start, range.startCol), end + jsOffsetForUtf8Column(source, end, range.endCol));
188
+ }
189
+ /** Convert a Tree-sitter UTF-8 column into a JS string offset on that line. */
190
+ function jsOffsetForUtf8Column(source, lineStart, utf8Column) {
191
+ let bytes = 0;
192
+ let index = lineStart;
193
+ while (index < source.length && bytes < utf8Column) {
194
+ if (source[index] === '\n')
195
+ break;
196
+ const codePoint = source.codePointAt(index);
197
+ if (codePoint === undefined)
198
+ break;
199
+ bytes += utf8ByteLength(codePoint);
200
+ index += codePoint > 0xffff ? 2 : 1;
201
+ }
202
+ return index - lineStart;
203
+ }
204
+ function utf8ByteLength(codePoint) {
205
+ if (codePoint <= 0x7f)
206
+ return 1;
207
+ if (codePoint <= 0x7ff)
208
+ return 2;
209
+ if (codePoint <= 0xffff)
210
+ return 3;
211
+ return 4;
212
+ }
213
+ /** Header through the first unquoted `{`, with strings and comments blanked. */
214
+ function cleanExtensionHeader(declaration) {
215
+ let index = 0;
216
+ let cleaned = '';
217
+ const blank = (from, to) => {
218
+ for (let cursor = from; cursor < to; cursor += 1) {
219
+ cleaned += declaration[cursor] === '\n' ? '\n' : ' ';
220
+ }
221
+ };
222
+ while (index < declaration.length) {
223
+ const current = declaration[index];
224
+ if (current === '/' && declaration[index + 1] === '/') {
225
+ const end = skipLineComment(declaration, index);
226
+ blank(index, end);
227
+ index = end;
228
+ continue;
229
+ }
230
+ if (current === '/' && declaration[index + 1] === '*') {
231
+ const end = skipBlockComment(declaration, index);
232
+ blank(index, end);
233
+ index = end;
234
+ continue;
235
+ }
236
+ const pounds = current === '#' ? leadingPounds(declaration, index) : 0;
237
+ const quoteAt = index + pounds;
238
+ if (declaration.startsWith('"""', quoteAt) || declaration[quoteAt] === '"') {
239
+ const end = skipSwiftString(declaration, index);
240
+ blank(index, end);
241
+ index = end;
242
+ continue;
243
+ }
244
+ if (current === "'") {
245
+ const end = skipQuoted(declaration, index, current);
246
+ blank(index, end);
247
+ index = end;
248
+ continue;
249
+ }
250
+ if (current === '{')
251
+ break;
252
+ cleaned += current;
253
+ index += 1;
254
+ }
255
+ return cleaned;
256
+ }
257
+ function skipLineComment(text, start) {
258
+ const newline = text.indexOf('\n', start);
259
+ return newline === -1 ? text.length : newline + 1;
260
+ }
261
+ function skipBlockComment(text, start) {
262
+ let index = start + 2;
263
+ let depth = 1;
264
+ while (index < text.length && depth > 0) {
265
+ if (text.startsWith('/*', index)) {
266
+ depth += 1;
267
+ index += 2;
268
+ continue;
269
+ }
270
+ if (text.startsWith('*/', index)) {
271
+ depth -= 1;
272
+ index += 2;
273
+ continue;
274
+ }
275
+ index += 1;
276
+ }
277
+ return index;
278
+ }
279
+ function leadingPounds(text, start) {
280
+ let count = 0;
281
+ while (text[start + count] === '#')
282
+ count += 1;
283
+ return count;
284
+ }
285
+ function skipSwiftString(text, start) {
286
+ const pounds = leadingPounds(text, start);
287
+ const quoteAt = start + pounds;
288
+ if (text.startsWith('"""', quoteAt)) {
289
+ return skipDelimitedString(text, quoteAt + 3, `"""${'#'.repeat(pounds)}`, pounds === 0);
290
+ }
291
+ if (text[quoteAt] === '"') {
292
+ return skipDelimitedString(text, quoteAt + 1, `"${'#'.repeat(pounds)}`, pounds === 0);
293
+ }
294
+ return start + Math.max(pounds, 1);
295
+ }
296
+ function skipDelimitedString(text, bodyStart, closer, escapes) {
297
+ let index = bodyStart;
298
+ while (index < text.length) {
299
+ if (escapes && text[index] === '\\') {
300
+ index += 2;
301
+ continue;
302
+ }
303
+ if (text.startsWith(closer, index))
304
+ return index + closer.length;
305
+ index += 1;
306
+ }
307
+ return text.length;
308
+ }
309
+ function skipQuoted(text, start, quote) {
310
+ let index = start + 1;
311
+ while (index < text.length) {
312
+ if (text[index] === '\\') {
313
+ index += 2;
314
+ continue;
315
+ }
316
+ if (text[index] === quote)
317
+ return index + 1;
318
+ index += 1;
319
+ }
320
+ return text.length;
321
+ }
322
+ function logicalOwnerKey(def) {
323
+ const qualifiedName = def.qualifiedName ?? def.nodeId;
324
+ const namespacePrefix = def.namespacePrefix ?? '';
325
+ return `${namespacePrefix.length}:${namespacePrefix}:${qualifiedName}`;
326
+ }
327
+ function simpleName(def) {
328
+ return def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
329
+ }
330
+ function addNamespaceBinding(augmentations, scopeId, def) {
331
+ const name = simpleName(def);
332
+ if (name === '')
333
+ return;
334
+ const bucket = getAugmentationBucket(augmentations, scopeId, name);
335
+ if (bucket.some((binding) => binding.def.nodeId === def.nodeId))
336
+ return;
337
+ bucket.push({ def, origin: 'namespace' });
338
+ }
59
339
  function getAugmentationBucket(augmentations, scopeId, name) {
60
340
  let scopeBindings = augmentations.get(scopeId);
61
341
  if (scopeBindings === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.20",
3
+ "version": "1.6.13-rc.21",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",