mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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)) &&
|
||||
|
||||
Reference in New Issue
Block a user