codedeep-mcp 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/config.js +223 -0
  4. package/dist/git/analyzer.js +177 -0
  5. package/dist/git/git-service.js +568 -0
  6. package/dist/git/head-watcher.js +113 -0
  7. package/dist/git/runner.js +204 -0
  8. package/dist/index.js +138 -0
  9. package/dist/indexer/code-index.js +1801 -0
  10. package/dist/indexer/complexity.js +633 -0
  11. package/dist/indexer/extractor.js +354 -0
  12. package/dist/indexer/languages/cpp.js +934 -0
  13. package/dist/indexer/languages/csharp.js +854 -0
  14. package/dist/indexer/languages/dart.js +777 -0
  15. package/dist/indexer/languages/go.js +665 -0
  16. package/dist/indexer/languages/java.js +507 -0
  17. package/dist/indexer/languages/kotlin.js +709 -0
  18. package/dist/indexer/languages/objc.js +397 -0
  19. package/dist/indexer/languages/php.js +771 -0
  20. package/dist/indexer/languages/python.js +455 -0
  21. package/dist/indexer/languages/ruby.js +697 -0
  22. package/dist/indexer/languages/rust.js +754 -0
  23. package/dist/indexer/languages/swift.js +691 -0
  24. package/dist/indexer/languages/typescript.js +485 -0
  25. package/dist/indexer/parser.js +175 -0
  26. package/dist/indexer/pipeline.js +342 -0
  27. package/dist/indexer/scanner.js +279 -0
  28. package/dist/indexer/watcher.js +353 -0
  29. package/dist/logger.js +16 -0
  30. package/dist/server.js +170 -0
  31. package/dist/tools/common.js +207 -0
  32. package/dist/tools/find-references.js +224 -0
  33. package/dist/tools/find-symbol.js +94 -0
  34. package/dist/tools/get-context.js +370 -0
  35. package/dist/tools/impact.js +218 -0
  36. package/dist/tools/overview.js +482 -0
  37. package/dist/tools/search-structure.js +303 -0
  38. package/dist/types.js +61 -0
  39. package/grammars/tree-sitter-c.wasm +0 -0
  40. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  41. package/grammars/tree-sitter-cpp.wasm +0 -0
  42. package/grammars/tree-sitter-dart.wasm +0 -0
  43. package/grammars/tree-sitter-go.wasm +0 -0
  44. package/grammars/tree-sitter-java.wasm +0 -0
  45. package/grammars/tree-sitter-javascript.wasm +0 -0
  46. package/grammars/tree-sitter-kotlin.wasm +0 -0
  47. package/grammars/tree-sitter-objc.wasm +0 -0
  48. package/grammars/tree-sitter-php.wasm +0 -0
  49. package/grammars/tree-sitter-python.wasm +0 -0
  50. package/grammars/tree-sitter-ruby.wasm +0 -0
  51. package/grammars/tree-sitter-rust.wasm +0 -0
  52. package/grammars/tree-sitter-swift.wasm +0 -0
  53. package/grammars/tree-sitter-tsx.wasm +0 -0
  54. package/grammars/tree-sitter-typescript.wasm +0 -0
  55. package/package.json +67 -0
@@ -0,0 +1,397 @@
1
+ import { collectAmbiguousTypeNames, normalizeSignature, resolveCalls, } from '../extractor.js';
2
+ import { IMPORT_NAMESPACE, RECEIVER_OPAQUE } from '../../types.js';
3
+ import { analyze, cppDoc, cppMemberCallInfo, handleMember, makeCppSymbol, memberFqn, topFqn, CFAMILY_COMPLEXITY_OPTS, CPP_FUNCTION_BODY_SKIP_TYPES, CPP_SKIP_TYPES, PREPROC_GROUPS, } from './cpp.js';
4
+ import { computeComplexity } from '../complexity.js';
5
+ // ── skip sets / call-resolution knobs ───────────────────────────────────────
6
+ // resolveCalls walk skip set: the C-subset bodies/conditionals (CPP_SKIP_TYPES,
7
+ // incl. the `preproc_*` spurious-`#if FOO(3)`-edge guard) PLUS the Objective-C OO
8
+ // container bodies — each method owns its own PendingBody, so the moduleRoot walk
9
+ // must not re-descend into them. `block_literal` is deliberately ABSENT → DESCENDED
10
+ // (a `^{ … }` closure's calls roll into the enclosing method, the Go func_literal rule).
11
+ const OBJC_SKIP_TYPES = new Set([
12
+ ...CPP_SKIP_TYPES,
13
+ 'class_interface',
14
+ 'class_implementation',
15
+ 'protocol_declaration',
16
+ 'method_definition',
17
+ ]);
18
+ // A bare `foo()` in Objective-C is ALWAYS a C free function — ObjC has NO
19
+ // implicit-this (a sibling method is called via `[self foo]`, a message_expression).
20
+ // So the callee stays the engine-default `identifier` and binds over {function} only,
21
+ // with `bareCallsBindToEnclosingClass:false` (THE divergence from cpp).
22
+ const OBJC_BARE_CALLEE_TYPES = new Set(['identifier']);
23
+ const OBJC_BARE_CALLABLE_KINDS = new Set(['function']);
24
+ // libc free functions + the Foundation logging/assert macros that parse as bare
25
+ // `identifier` calls and never resolve to a local symbol. Suppressed ONLY when
26
+ // unresolved (a file-local shadow keeps its refs). START small + tune by dogfood.
27
+ const OBJC_IGNORED_BARE_CALLEES = new Set([
28
+ 'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf', 'sscanf', 'puts', 'fputs',
29
+ 'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memset', 'memmove', 'memcmp',
30
+ 'strlen', 'strcmp', 'strncmp', 'strcpy', 'strncpy', 'strcat', 'abort', 'exit',
31
+ 'NSLog', 'NSLogv', 'NSAssert', 'NSCAssert', 'NSParameterAssert', 'NSCParameterAssert',
32
+ 'NSStringFromClass', 'NSStringFromSelector', 'NSStringFromProtocol', 'NSClassFromString',
33
+ ]);
34
+ // Construction + memory-management + NSObject-protocol selectors that flood the
35
+ // member-ref store when unresolved (`[[Foo alloc] init]`, retain/release in MRC code,
36
+ // reflection). Suppressed ONLY when UNRESOLVED — a same-file `[self copy]` that bound
37
+ // to a real sibling keeps its ref. Note the FULL keyword selector `initWithName:age:`
38
+ // is NOT in this set, so designated initializers still resolve. START small.
39
+ const OBJC_IGNORED_MEMBER_CALLEES = new Set([
40
+ 'alloc', 'init', 'new', 'retain', 'release', 'autorelease', 'dealloc', 'copy',
41
+ 'mutableCopy', 'class', 'superclass', 'description', 'debugDescription', 'hash',
42
+ 'isEqual', 'respondsToSelector', 'conformsToProtocol', 'isKindOfClass',
43
+ 'isMemberOfClass', 'performSelector',
44
+ ]);
45
+ const OBJC_SELECTORS = [
46
+ // C calls (bare `foo()`) and C-subset member calls (`p->fn()` — discriminated by
47
+ // the callee node, delegated to cppMemberCallInfo).
48
+ { nodeType: 'call_expression', getCallee: (n) => n.childForFieldName('function') },
49
+ // Objective-C message sends `[recv sel:arg]`. The selector is spread across the
50
+ // node, so the callee IS the node (objcMemberCallInfo reads it).
51
+ { nodeType: 'message_expression', getCallee: (n) => n },
52
+ ];
53
+ // File scope: a C function/struct/global in a `.m`/`.h` is never inside an ObjC
54
+ // `@interface` body, so there is no positional access state — `inClass:false` routes
55
+ // `declExported` to `!hasStaticStorage` (C internal linkage). Shared, never mutated.
56
+ const FILE_SCOPE = {
57
+ className: null,
58
+ qualifier: '',
59
+ exported: true,
60
+ inClass: false,
61
+ defaultVisibility: 'public',
62
+ };
63
+ // ── selector reconstruction (THE byte-identity invariant) ────────────────────
64
+ // The selector concat rule MUST be byte-identical on the declaration side
65
+ // (method_declaration / method_definition) and the call side (message_expression),
66
+ // or `[self initWithName:age:]` would never resolve to the `initWithName:age:`
67
+ // method. `joinSelector` is the SOLE place a `:` is appended.
68
+ function joinSelector(labels, hasArgs) {
69
+ return hasArgs ? labels.map((l) => `${l}:`).join('') : labels.join('');
70
+ }
71
+ // Declaration side: the labels are the method node's DIRECT `identifier` children
72
+ // (the leading selector segment + one bare identifier before each keyword
73
+ // `method_parameter`); the parameter NAMES live INSIDE each `method_parameter`, not
74
+ // as direct children. `hasArgs` ⟺ the method takes ≥1 `method_parameter`.
75
+ function selectorFromMethodDecl(node) {
76
+ const labels = [];
77
+ let hasArgs = false;
78
+ for (const c of node.namedChildren) {
79
+ if (c.type === 'identifier')
80
+ labels.push(c.text);
81
+ else if (c.type === 'method_parameter')
82
+ hasArgs = true;
83
+ }
84
+ return joinSelector(labels, hasArgs);
85
+ }
86
+ // Call side: the labels are the `method:` fields; `hasArgs` ⟺ the message carries a
87
+ // `:` token (a keyword message has one per labelled argument; a unary message — `[x
88
+ // draw]` — has none, so its single `method` field stays colon-free).
89
+ function selectorFromMessage(node) {
90
+ const labels = node.childrenForFieldName('method').map((m) => m.text);
91
+ const hasArgs = node.children.some((c) => c.type === ':');
92
+ return joinSelector(labels, hasArgs);
93
+ }
94
+ // ── call resolution ──────────────────────────────────────────────────────────
95
+ // Reduces an Objective-C message send OR a C-subset member callee to {receiver,
96
+ // property, isSelf}. Message sends:
97
+ // `[self sel]` → self-call (resolve against the enclosing @implementation)
98
+ // `[super sel]` → null (parent dispatch — the cpp super:: drop rule)
99
+ // `[Greeter make]` → receiver = `Greeter` (a class send RESOLVES via
100
+ // methodsByClass['Greeter'])
101
+ // `[obj greet]` → receiver = `obj` (an instance send — stays
102
+ // unresolved-but-findable: dynamic typing, no receiver type)
103
+ // `[[Foo alloc] init]` → RECEIVER_OPAQUE (nested-message receiver — findable, never
104
+ // resolved; the construction recall gap)
105
+ // C-subset member callees (`p->fn()`, `Foo::bar()`) → the shared cppMemberCallInfo.
106
+ function objcMemberCallInfo(callee) {
107
+ if (callee.type === 'message_expression') {
108
+ const property = selectorFromMessage(callee);
109
+ if (!property)
110
+ return null;
111
+ const receiver = callee.childForFieldName('receiver');
112
+ if (!receiver)
113
+ return null;
114
+ if (receiver.type === 'identifier') {
115
+ const text = receiver.text;
116
+ if (text === 'self')
117
+ return { receiver: 'self', property, isSelf: true };
118
+ if (text === 'super')
119
+ return null;
120
+ return { receiver: text, property, isSelf: false };
121
+ }
122
+ // nested message / field / computed receiver → opaque (findable, never resolved)
123
+ return { receiver: RECEIVER_OPAQUE, property, isSelf: false };
124
+ }
125
+ return cppMemberCallInfo(callee);
126
+ }
127
+ // ── entry point ────────────────────────────────────────────────────────────
128
+ export function extractObjc(tree, content, fileInfo) {
129
+ const ctx = {
130
+ content,
131
+ fileInfo,
132
+ occurrences: new Map(),
133
+ symbols: [],
134
+ imports: [],
135
+ bodies: [],
136
+ };
137
+ // C-subset nodes route through cpp's handleMember, which mutates a visibility state
138
+ // for in-class access labels — irrelevant at ObjC file scope, but the signature
139
+ // needs it. One shared object suffices (never flipped at top level).
140
+ const state = { visibility: 'public' };
141
+ // A class is declared by an `@interface` AND defined by an `@implementation`; in ONE
142
+ // file (a private helper class, a single-file program, an umbrella header) BOTH would
143
+ // emit a `class` symbol named C, making C ambiguous → excluded from methodsByClass →
144
+ // every `[self …]`/`[C …]` send fails to resolve. Emit the class symbol only ONCE per
145
+ // name per file (first-wins); both blocks still contribute members. Cross-file
146
+ // interface/impl stay separate symbols (separate extractions) — the decl/def split.
147
+ const emittedClasses = new Set();
148
+ for (const child of tree.rootNode.namedChildren)
149
+ handleTopLevel(ctx, child, state, emittedClasses);
150
+ // Same-name classes share the simple-name FQN; resolving through them first-wins
151
+ // would bind to the WRONG class (the cpp/Go/C# pattern). Categories/class-extensions
152
+ // do NOT emit a duplicate class symbol — they merge into the existing class — so the
153
+ // only genuine duplicates are distinct same-name @interface/@implementation blocks.
154
+ const ambiguousClassNames = collectAmbiguousTypeNames(ctx.symbols, new Set(['class']));
155
+ const references = resolveCalls(ctx.bodies, tree.rootNode, ctx.symbols, fileInfo, OBJC_SELECTORS, OBJC_SKIP_TYPES, CPP_FUNCTION_BODY_SKIP_TYPES, objcMemberCallInfo, {
156
+ bareCalleeTypes: OBJC_BARE_CALLEE_TYPES,
157
+ plainCalleeType: 'identifier',
158
+ bareCallableKinds: OBJC_BARE_CALLABLE_KINDS,
159
+ bareCallsBindToEnclosingClass: false, // ObjC has NO implicit-this
160
+ ambiguousClassNames,
161
+ ignoredBareCallees: OBJC_IGNORED_BARE_CALLEES,
162
+ ignoredMemberCallees: OBJC_IGNORED_MEMBER_CALLEES,
163
+ // NO constructorKinds/constructorSelectorTypes — `[[Foo alloc] init]` is a nested
164
+ // message send (opaque), not a distinct construction node.
165
+ });
166
+ // Cyclomatic + cognitive complexity — the SAME shared `CFAMILY_COMPLEXITY_OPTS` as cpp/c
167
+ // (one source of truth in cpp.ts). The AST dump confirmed tree-sitter-objc reuses the C
168
+ // control-flow node names (incl. `catch_clause` for `@catch`), so the shared options apply
169
+ // unchanged; objc method bodies are `method_definition` PendingBodies (NOT in the skip set,
170
+ // so they're descended) and `block_literal` (`^{}`) nests +0 like a C++ lambda.
171
+ computeComplexity(ctx.bodies, ctx.symbols, CFAMILY_COMPLEXITY_OPTS);
172
+ return { symbols: ctx.symbols, references, imports: ctx.imports };
173
+ }
174
+ function handleTopLevel(ctx, node, state, emittedClasses) {
175
+ const t = node.type;
176
+ // `#ifndef GUARD … #endif` include guards / `#if` blocks wrap the real declarations
177
+ // (the dominant ObjC header shape). Recurse transparently so an @interface inside a
178
+ // guard is still reached — handleMember can't (it has no OO cases). Both branches are
179
+ // extracted (over-extraction; the OccurrenceCounter keeps ids unique).
180
+ if (PREPROC_GROUPS.has(t)) {
181
+ for (const c of node.namedChildren)
182
+ handleTopLevel(ctx, c, state, emittedClasses);
183
+ return;
184
+ }
185
+ switch (t) {
186
+ case 'class_interface':
187
+ extractInterface(ctx, node, emittedClasses);
188
+ return;
189
+ case 'class_implementation':
190
+ extractImplementation(ctx, node, emittedClasses);
191
+ return;
192
+ case 'protocol_declaration':
193
+ extractProtocol(ctx, node);
194
+ return;
195
+ case 'module_import':
196
+ extractModuleImport(ctx, node);
197
+ return;
198
+ case 'class_declaration': // `@class A, B;` forward declaration
199
+ case 'protocol_forward_declaration': // `@protocol P;` forward declaration
200
+ return;
201
+ case 'type_definition': {
202
+ // `typedef NS_ENUM(NSInteger, Color) { … }` — the macro is opaque to the grammar
203
+ // (mis-parses; the enum NAME + members are buried in an ERROR). Skip the whole
204
+ // typedef rather than emit its mis-parsed enumerators as spurious `type` symbols
205
+ // (a documented macro-opacity recall gap, never a wrong edge).
206
+ if (node.childForFieldName('type')?.type === 'macro_type_specifier')
207
+ return;
208
+ handleMember(ctx, node, FILE_SCOPE, state, null);
209
+ return;
210
+ }
211
+ default:
212
+ // C subset (function_definition / declaration / struct / union / enum / typedef)
213
+ // + `#import` → the shared cpp dispatcher at file scope. Inert on the OO /
214
+ // namespace / template nodes it doesn't recognize.
215
+ handleMember(ctx, node, FILE_SCOPE, state, null);
216
+ return;
217
+ }
218
+ }
219
+ // ── Objective-C OO surface ───────────────────────────────────────────────────
220
+ // `@interface C : Super <P…> {ivars} … @end`, a category `@interface C (Cat)`, OR a
221
+ // class-extension `@interface C ()` — all are `class_interface`. The class NAME is the
222
+ // first `identifier` child (no `name:` field); `superclass:`/`category:`/protocol
223
+ // arguments are display-only. A category/extension (it carries `(`/`)` tokens) emits
224
+ // NO class symbol — it only merges members into the existing class via the shared
225
+ // simple-name FQN `file:C.<member>` (the Go/Swift extension-apart pattern).
226
+ function extractInterface(ctx, node, emittedClasses) {
227
+ const className = firstIdentifier(node);
228
+ if (!className)
229
+ return;
230
+ const isCategoryOrExtension = node.children.some((c) => c.type === '(');
231
+ if (!isCategoryOrExtension && !emittedClasses.has(className)) {
232
+ emittedClasses.add(className);
233
+ ctx.symbols.push(makeCppSymbol(ctx, node, headSignature(ctx, node), 'class', className, topFqn(ctx, className), true, cppDoc(node), className));
234
+ }
235
+ extractMembers(ctx, node, className);
236
+ }
237
+ // `@implementation C … @end` (or a category implementation `@implementation C (Cat)`).
238
+ // A NON-category impl emits the class symbol (a `.m`-only private class has no
239
+ // `@interface`; a header decl is in a DIFFERENT file so its id differs by path; a
240
+ // same-file @interface/@implementation pair emits the class symbol only ONCE via the
241
+ // shared `emittedClasses` set, first-wins). A CATEGORY impl (it carries `(`/`)` tokens)
242
+ // emits NO class symbol either — the class is defined elsewhere; a second same-name
243
+ // `class` symbol in one file would make `C` ambiguous and EXCLUDE its methods from
244
+ // methodsByClass, breaking every `[self …]`/`[C …]` resolution. Methods are wrapped one
245
+ // level deep in `implementation_definition` (handled by extractMembers).
246
+ function extractImplementation(ctx, node, emittedClasses) {
247
+ const className = firstIdentifier(node);
248
+ if (!className)
249
+ return;
250
+ const isCategory = node.children.some((c) => c.type === '(');
251
+ if (!isCategory && !emittedClasses.has(className)) {
252
+ emittedClasses.add(className);
253
+ ctx.symbols.push(makeCppSymbol(ctx, node, headSignature(ctx, node), 'class', className, topFqn(ctx, className), true, cppDoc(node), className));
254
+ }
255
+ extractMembers(ctx, node, className);
256
+ }
257
+ // `@protocol P <Base> … @end` → an `interface` symbol with declaration-only members.
258
+ // Members sit under `qualified_protocol_interface_declaration` wrappers (one per
259
+ // `@required`/`@optional`), recursed transparently by extractMembers.
260
+ function extractProtocol(ctx, node) {
261
+ const name = firstIdentifier(node);
262
+ if (!name)
263
+ return;
264
+ ctx.symbols.push(makeCppSymbol(ctx, node, headSignature(ctx, node), 'interface', name, topFqn(ctx, name), true, cppDoc(node), name));
265
+ extractMembers(ctx, node, name);
266
+ }
267
+ // Walks an @interface / @implementation / @protocol body for method + property
268
+ // members. Methods are DIRECT children (interfaces, categories) or wrapped one level
269
+ // deep in `implementation_definition` (impl) / `qualified_protocol_interface_declaration`
270
+ // (a protocol's @required/@optional block) — descend those transparently. ivars
271
+ // (`instance_variables`) are NOT extracted in v1 (private implementation detail; the
272
+ // public API surface is `@property`).
273
+ function extractMembers(ctx, container, className) {
274
+ for (const child of container.namedChildren) {
275
+ switch (child.type) {
276
+ case 'implementation_definition':
277
+ case 'qualified_protocol_interface_declaration':
278
+ extractMembers(ctx, child, className);
279
+ break;
280
+ case 'method_declaration':
281
+ case 'method_definition':
282
+ extractMethod(ctx, child, className);
283
+ break;
284
+ case 'property_declaration':
285
+ extractProperty(ctx, child, className);
286
+ break;
287
+ default:
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ // A `method_declaration` (header, bodiless) or `method_definition` (impl, with a
293
+ // `compound_statement` body). BOTH `+` (class) and `-` (instance) → **method** kind
294
+ // keyed on the class (the Ruby `def self.x` precedent — a class send `[C sel]` resolves
295
+ // via methodsByClass['C']). Name = the full selector. The decl→def cross-file pairing
296
+ // produces two symbols for one method (the documented C++ decl/def split); same-file
297
+ // duplicates dedup via the OccurrenceCounter.
298
+ function extractMethod(ctx, node, className) {
299
+ const selector = selectorFromMethodDecl(node);
300
+ if (!selector)
301
+ return;
302
+ const sym = makeCppSymbol(ctx, node, methodSignature(ctx, node), 'method', selector, memberFqn(ctx, className, selector), true, cppDoc(node), className);
303
+ ctx.symbols.push(sym);
304
+ // The impl `method_definition` owns a PendingBody (className set so `[self sel]`
305
+ // resolves same-file); the header `method_declaration` is bodiless (the cross-file
306
+ // decl→def self-call gap, the C++ precedent).
307
+ if (node.type === 'method_definition' && node.namedChildren.some((c) => c.type === 'compound_statement')) {
308
+ ctx.bodies.push({ symbolId: sym.id, body: node, className });
309
+ }
310
+ }
311
+ // `@property (attrs) Type *name;` → a **variable** member `file:C.name`. The name lives
312
+ // in a C-style `struct_declaration > struct_declarator > (identifier | pointer/…
313
+ // declarator)` — `analyze` resolves it through any pointer/function-pointer wrapper.
314
+ // v1 emits the variable only; synthesizing getter/setter `method` symbols (`name`/
315
+ // `setName:`) so a dotted/message access resolves is a noted v2.
316
+ function extractProperty(ctx, node, className) {
317
+ const sd = node.namedChildren.find((c) => c.type === 'struct_declaration');
318
+ if (!sd)
319
+ return;
320
+ const sig = propertySignature(ctx, node);
321
+ const doc = cppDoc(node);
322
+ // One variable per declarator: `@property (assign) int a, b;` declares both a and b.
323
+ // `analyze` resolves the name through any pointer / block-pointer / function-pointer
324
+ // wrapper (`void (^handler)(int)` → `handler`).
325
+ for (const declarator of sd.namedChildren) {
326
+ if (declarator.type !== 'struct_declarator')
327
+ continue;
328
+ const inner = declarator.namedChildren[0];
329
+ if (!inner)
330
+ continue;
331
+ const info = analyze(inner);
332
+ if (!info || !info.name)
333
+ continue;
334
+ ctx.symbols.push(makeCppSymbol(ctx, node, sig, 'variable', info.name, memberFqn(ctx, className, info.name), true, doc, className));
335
+ }
336
+ }
337
+ // `@import Module;` → a namespace-style import (a module has no named binding). The
338
+ // `path:` field is the module name (`UIKit`, `Foo.Bar`). `#import`/`#include` are
339
+ // handled by the shared extractInclude via handleMember.
340
+ function extractModuleImport(ctx, node) {
341
+ // The `path:` field has MULTIPLE children for a dotted submodule import
342
+ // (`@import Foo.Bar;` → identifier `Foo`, `.`, identifier `Bar`); the `.` tokens are
343
+ // themselves path children, so a plain join reproduces the full `Foo.Bar`.
344
+ const segments = node.childrenForFieldName('path');
345
+ if (segments.length === 0)
346
+ return;
347
+ const sourceModule = segments.map((c) => c.text).join('');
348
+ if (!sourceModule)
349
+ return;
350
+ ctx.imports.push({
351
+ file: ctx.fileInfo.path,
352
+ sourceModule,
353
+ importedNames: [{ name: IMPORT_NAMESPACE }],
354
+ line: node.startPosition.row + 1,
355
+ });
356
+ }
357
+ // ── helpers ───────────────────────────────────────────────────────────────
358
+ function firstIdentifier(node) {
359
+ for (const c of node.namedChildren) {
360
+ if (c.type === 'identifier')
361
+ return c.text;
362
+ }
363
+ return null;
364
+ }
365
+ // Member nodes / `@end` / the ivar block that mark the END of an @interface /
366
+ // @protocol / @implementation HEAD line. cppSignature is unusable on these OO nodes
367
+ // (no `body:` field — it would slurp the whole interface), so cut at the first such.
368
+ const HEAD_STOP = new Set([
369
+ 'instance_variables',
370
+ 'property_declaration',
371
+ 'method_declaration',
372
+ 'method_definition',
373
+ 'implementation_definition',
374
+ 'qualified_protocol_interface_declaration',
375
+ '@end',
376
+ ]);
377
+ // Signature for an @interface/@protocol/@implementation head: source from the node
378
+ // start to the first member / ivar block / `@end`, trimmed of a trailing `{`.
379
+ function headSignature(ctx, node) {
380
+ let cut = node.endIndex;
381
+ for (const c of node.children) {
382
+ if (HEAD_STOP.has(c.type))
383
+ cut = Math.min(cut, c.startIndex);
384
+ }
385
+ return normalizeSignature(ctx.content.slice(node.startIndex, cut)).replace(/\{\s*$/, '').trimEnd();
386
+ }
387
+ // `- (Type)selector …` up to the body (a definition) or end (a declaration), trimmed
388
+ // of a trailing `;`.
389
+ function methodSignature(ctx, node) {
390
+ const body = node.namedChildren.find((c) => c.type === 'compound_statement');
391
+ const cut = body ? body.startIndex : node.endIndex;
392
+ return normalizeSignature(ctx.content.slice(node.startIndex, cut)).replace(/;\s*$/, '').trimEnd();
393
+ }
394
+ // The whole `@property (…) Type *name` line, trimmed of a trailing `;`.
395
+ function propertySignature(ctx, node) {
396
+ return normalizeSignature(ctx.content.slice(node.startIndex, node.endIndex)).replace(/;\s*$/, '').trimEnd();
397
+ }