Просмотр исходного кода

fix(extraction): multi-keyword ObjC message_expression resolves to method definition

#165 extracted multi-part ObjC selectors correctly on the *definition*
side (`GET:parameters:headers:progress:success:failure:` as a method
node name) but the call-site handler in tree-sitter.ts only built the
first selector keyword, so calls never resolved to those definitions.
Verified on AFNetworking: 0 → 84 call edges targeting multi-keyword
methods after the fix.

The grammar emits one `method` field child per keyword on
message_expression nodes; collecting them all and joining with `:`
reconstructs the full selector (matching what extractObjcMethodName
does on the definition side).

Regression test in extraction.test.ts covers
`[d setObject:@"v" forKey:@"k"]` → `d.setObject:forKey:`,
the 3-keyword form, and the self/super-skip case
(`touchesBegan:withEvent:`).

This is a prerequisite for Swift↔ObjC bridging (the bridge rides the
same call-edge path), but stands on its own as a #165 followup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 4 недель назад
Родитель
Сommit
036c4976d6
2 измененных файлов с 49 добавлено и 3 удалено
  1. 29 0
      __tests__/extraction.test.ts
  2. 20 3
      src/extraction/tree-sitter.ts

+ 29 - 0
__tests__/extraction.test.ts

@@ -3987,6 +3987,35 @@ void helperFunction(int count) {
     expect(calls).toEqual(expect.arrayContaining(['NSLog', 'doWork', 'MyClass.shared', 'obj.greet']));
     expect(calls).toEqual(expect.arrayContaining(['NSLog', 'doWork', 'MyClass.shared', 'obj.greet']));
   });
   });
 
 
+  it('should reconstruct multi-keyword selectors at the call site so they resolve to the method definition', () => {
+    // Regression for the gap discovered post-#165: message_expression's
+    // multi-keyword form `[obj a:1 b:2]` was only emitting the first keyword,
+    // so calls never resolved to multi-part method definitions like
+    // `GET:parameters:headers:progress:success:failure:`. The call-site name
+    // must match the method-definition name with full keywords + trailing colons.
+    const code = `
+@implementation Caller
+- (void)demo {
+    NSMutableDictionary *d = [NSMutableDictionary new];
+    [d setObject:@"v" forKey:@"k"];
+    [d setObject:@"v2" forKey:@"k2" withRetry:@YES];
+    [self touchesBegan:nil withEvent:nil];
+}
+@end
+`;
+    const result = extractFromSource('Caller.m', code);
+    const calls = result.unresolvedReferences
+      .filter((r) => r.referenceKind === 'calls')
+      .map((r) => r.referenceName);
+    expect(calls).toEqual(
+      expect.arrayContaining([
+        'd.setObject:forKey:',
+        'd.setObject:forKey:withRetry:',
+        'touchesBegan:withEvent:',
+      ])
+    );
+  });
+
   it('should not classify pure C headers with @end in comments as objc', () => {
   it('should not classify pure C headers with @end in comments as objc', () => {
     const cHeader = '/* @end of file */\n#ifndef STDIO_H\nvoid printf(const char *);\n#endif\n';
     const cHeader = '/* @end of file */\n#ifndef STDIO_H\nvoid printf(const char *);\n#endif\n';
     expect(detectLanguage('stdio.h', cHeader)).toBe('c');
     expect(detectLanguage('stdio.h', cHeader)).toBe('c');

+ 20 - 3
src/extraction/tree-sitter.ts

@@ -1467,9 +1467,26 @@ export class TreeSitterExtractor {
         }
         }
       }
       }
     } else if (node.type === 'message_expression') {
     } else if (node.type === 'message_expression') {
-      const methodField = getChildByField(node, 'method');
-      if (methodField) {
-        const methodName = getNodeText(methodField, this.source);
+      // ObjC message expressions emit one `method` field child per selector
+      // keyword: `[obj a:1 b:2 c:3]` has three `method=identifier` siblings.
+      // Joining them with `:` reconstructs the full selector and matches the
+      // multi-part selector names produced by the ObjC method_definition
+      // extractor (`extractObjcMethodName` in languages/objc.ts). Without this
+      // join, multi-keyword call sites only emitted the first keyword and never
+      // resolved to their target methods (e.g. `GET:parameters:headers:...` had
+      // zero callers despite obviously being called).
+      const methodKeywords: string[] = [];
+      for (let i = 0; i < node.namedChildCount; i++) {
+        if (node.fieldNameForNamedChild(i) === 'method') {
+          const kw = node.namedChild(i);
+          if (kw) methodKeywords.push(getNodeText(kw, this.source));
+        }
+      }
+      if (methodKeywords.length > 0) {
+        const methodName: string =
+          methodKeywords.length === 1
+            ? (methodKeywords[0] as string)
+            : methodKeywords.map((k) => `${k}:`).join('');
         const receiverField = getChildByField(node, 'receiver');
         const receiverField = getChildByField(node, 'receiver');
         const SKIP_RECEIVERS = new Set(['self', 'super']);
         const SKIP_RECEIVERS = new Set(['self', 'super']);
         if (receiverField && receiverField.type !== 'message_expression') {
         if (receiverField && receiverField.type !== 'message_expression') {