fix(objc): a message to super reaches the superclass (#2179)

`[super init]` was judged like a self-send, so the sending class's own
override passed (SDDiskCache's [super init] came back to itself), and
where tree-sitter-objc lost an @implementation's range no hierarchy was
read at all, so any class's init passed (NSObject subclasses' [super
init] -> SDDiskCache::init).

- a super-send excludes the class it is written in
- a lost @implementation's class is read from the enclosing method's
  qualified name (SDWebImageDownloaderDecryptor::initWithBlock:)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 17:04:59 +00:00
committed by GitHub
parent 2d0d120a35
commit de74c050bc
3 changed files with 109 additions and 11 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In Objective-C, `[super init]` and other messages to `super` now reach the superclass's method, never the sending class's own override. This also works where the index lost the `@implementation`'s range. On SDWebImage, `NSObject` subclasses' `[super init]` used to land on `SDDiskCache`'s `init`, and `[self class]` in tests on a proxy class's `class`. On AFNetworking, a serializer's `[super encodeWithCoder:]` now reaches its superclass. Re-index after upgrading.
- In Solidity, a call with no receiver now reaches a function of the contract around it or of one it inherits. On OpenZeppelin, `_msgSender()` in contracts built on `Context` used to land on `ERC2771Context`'s override 83 times. In Erlang, a call to another module's function now follows the file's `-import(Mod, [f/N])`. On cowboy, `-import(req_SUITE, [do_get/3])` calls used to land on another suite's `do_get/3`. A bare call with no such import no longer links to some other module. Re-index after upgrading.
- Calls on a library's own types and modules no longer land on a project method that happens to share the name. In Rust, `Vec::new()`, `String::from(…)` and module paths like `task::spawn(…)` stay the standard library's. A later link of a multi-line Rust or Go chain now counts as a chained call, named after the chain's first line. In Go, `w.Header().Get(…)` is net/http's. In R, a plain call like `range(x)` is a function, not an object's method. In JavaScript, `response.text()` / `.json()` is fetch's. On tokio, `task::spawn(…)` used to land on `TaskTracker::spawn` 300 times. On ggplot2, `range(…)` used to land on a coordinate system's `range` method. Re-index after upgrading.
- In C and C++, a qualified name like `fmt::format` now reaches a declaration inside a namespace the library opens with a macro, like fmt's `FMT_BEGIN_NAMESPACE`, pybind11's `PYBIND11_NAMESPACE_BEGIN(…)` or rapidjson's and spdlog's `…_NAMESPACE_BEGIN`. It also works through a namespace alias like `namespace py = pybind11;`. On fmt, 1,728 `fmt::format(…)` calls used to link to nothing, and so did about 1,400 `py::…` calls on pybind11.
+84
View File
@@ -0,0 +1,84 @@
/**
* `[super init]` goes past the class it is written in: to the superclass's
* `init`, or — for an NSObject subclass — to no project method at all.
* SDWebImage's NSObject subclasses sent `[super init]` to SDDiskCache's
* `init` (their `@implementation` range was lost, so no hierarchy was read
* and any `init` passed), and SDDiskCache's own `[super init]` came back to
* itself.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-super-'));
const files: Record<string, string> = {
'Base.h': `#import <Foundation/Foundation.h>
@interface Base : NSObject
- (instancetype)init;
@end
`,
'Base.m': `#import "Base.h"
@implementation Base
- (instancetype)init {
self = [super init];
return self;
}
@end
`,
'Child.h': `#import "Base.h"
@interface Child : Base
@end
`,
'Child.m': `#import "Child.h"
@implementation Child
- (instancetype)init {
self = [super init];
return self;
}
@end
`,
'Other.m': `#import <Foundation/Foundation.h>
@interface Other : NSObject
@end
@implementation Other
- (instancetype)init {
self = [super init];
return self;
}
@end
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `Owner::member` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('Objective-C messages to super', () => {
it('reach the superclass, never the sender itself or an unrelated class', () => {
expect(callsFrom('Child.m')).toEqual(['Base::init']);
expect(callsFrom('Base.m')).toEqual([]);
expect(callsFrom('Other.m')).toEqual([]);
});
});
+24 -11
View File
@@ -2590,19 +2590,20 @@ const OBJC_MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'property', 'f
* never a method), `self-send` for a message to `self` / `super` /
* `[self class]`, whose receiver the extractor drops; null otherwise.
*/
function objcCallShape(ref: UnresolvedRef, context: ResolutionContext): 'c-call' | 'self-send' | null {
function objcCallShape(ref: UnresolvedRef, context: ResolutionContext): 'c-call' | 'self-send' | 'super-send' | null {
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line === undefined) return null;
const name = ref.referenceName.split(':')[0]!;
if (!name) return null;
const at = new RegExp(`(?<![\\w.])${name}\\b`, 'g');
let m: RegExpExecArray | null;
let shape: 'c-call' | 'self-send' | null = null;
let shape: 'c-call' | 'self-send' | 'super-send' | null = null;
while ((m = at.exec(line))) {
const before = line.slice(0, m.index);
const after = line.slice(m.index + name.length);
if (/^\s*\(/.test(after) && !/\[\s*[\w.]+\s+$/.test(before)) shape ??= 'c-call';
else if (/\[\s*(?:self|super|\[\s*self\s+class\s*\])\s+$/.test(before)) return 'self-send';
else if (/\[\s*super\s+$/.test(before)) return 'super-send';
else if (/\[\s*(?:self|\[\s*self\s+class\s*\])\s+$/.test(before)) return 'self-send';
}
return shape;
}
@@ -2614,13 +2615,19 @@ function objcCallShape(ref: UnresolvedRef, context: ResolutionContext): 'c-call'
* the class they extend). Null outside any class.
*/
function objcHierarchyAt(ref: UnresolvedRef, context: ResolutionContext): Set<string> | null {
const here = context
.getNodesInFile(ref.filePath)
const inFile = context.getNodesInFile(ref.filePath);
let here = inFile
.filter((c) => c.kind === 'class' && c.startLine <= ref.line && c.endLine >= ref.line)
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0]?.name;
// An `@implementation` whose range the index lost still names its class in
// its methods: `SDWebImageDownloaderDecryptor::initWithBlock:`.
if (!here) {
const method = inFile.find((c) => c.kind === 'method' && c.startLine <= ref.line && c.endLine >= ref.line && c.qualifiedName.includes('::'));
here = method?.qualifiedName.slice(0, method.qualifiedName.lastIndexOf('::'));
}
if (!here) return null;
const seen = new Set<string>();
const queue = [here.name];
const queue = [here];
while (queue.length > 0 && seen.size < 30) {
const name = queue.shift()!;
if (seen.has(name)) continue;
@@ -2639,15 +2646,19 @@ function objcHierarchyAt(ref: UnresolvedRef, context: ResolutionContext): Set<st
* hierarchy, still counts (as does any, when the sender's own class was lost
* too). A function elsewhere is never what `[super init]` sends to.
*/
function isObjcSelfSendTarget(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
function isObjcSelfSendTarget(n: Node, ref: UnresolvedRef, context: ResolutionContext, toSuper = false): boolean {
const hierarchy = objcHierarchyAt(ref, context);
if (OBJC_MEMBER_KINDS.has(n.kind)) {
const cut = n.qualifiedName.lastIndexOf('::');
return cut < 0 || hierarchy === null || hierarchy.has(n.qualifiedName.slice(0, cut));
if (cut < 0 || hierarchy === null) return true;
const owner = n.qualifiedName.slice(0, cut);
// `[super init]` goes past the class it is written in.
return hierarchy.has(owner) && !(toSuper && owner === [...hierarchy][0]);
}
if (n.filePath === ref.filePath || hierarchy === null) return true;
if (n.filePath === ref.filePath) return !toSuper;
if (hierarchy === null) return true;
const base = n.filePath.slice(n.filePath.lastIndexOf('/') + 1).replace(/\.\w+$/, '');
return hierarchy.has(base);
return hierarchy.has(base) && !(toSuper && base === [...hierarchy][0]);
}
/**
@@ -3451,6 +3462,7 @@ export function matchByExactName(
!(csharpBare && !isCsharpMemberInScope(n, ref, context)) &&
!(objcShape === 'c-call' && OBJC_MEMBER_KINDS.has(n.kind)) &&
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
@@ -7061,6 +7073,7 @@ export function matchFuzzy(
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
!(objcShape === 'c-call' && OBJC_MEMBER_KINDS.has(n.kind)) &&
!(objcShape === 'self-send' && !isObjcSelfSendTarget(n, ref, context)) &&
!(objcShape === 'super-send' && !isObjcSelfSendTarget(n, ref, context, true)) &&
!(csharpBare && !isCsharpMemberInScope(n, ref, context)) &&
!(scalaBare && !isScalaMemberInScope(n, ref, context)) &&
!(rustGoShape && !isRustGoCallTarget(n, rustGoShape)) &&