Files
codegraph/__tests__/super-call-self-edge.test.ts
Colby Mchenry a4f9820233 fix(resolution): a super call in an override is not a self-call (#2136)
Extraction keeps `super.didMoveToWindow()` under the bare method name, so
every strategy resolved it to the enclosing override itself. Across the
bridge sweep that was 139 self-edges on Charts, 142 on commons-lang, 101
on BookStack, 92 on realm-swift, 82 on react-native-svg — lifecycle
overrides and delegates drawn as recursion.

`gateSuperSelfCall` declines a `calls` result that targets the calling
node when the call site is written through `super` / `base` (C#) /
`[super …]` (Objective-C) / `parent::` (PHP) / `super().` / `super(C, self).`
(Python). Plain recursion keeps its edge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:48:48 +00:00

97 lines
2.7 KiB
TypeScript

/**
* A `super` call inside an override calls the parent's implementation, never
* the method making the call.
*
* Extraction keeps `super.didMoveToWindow()` under the bare method name, so
* every strategy resolved it to the enclosing override itself — a self-edge
* that made a view's lifecycle methods look recursive (eleven of one
* expo-camera view's overrides). Real recursion keeps its edge.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});
async function project(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-super-self-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
/** `Owner.method` names of every method whose calls include itself. */
function selfCalling(cg: CodeGraph): string[] {
const out: string[] = [];
for (const kind of ['method', 'function'] as const) {
for (const n of cg.getNodesByKind(kind)) {
if (cg.getOutgoingEdges(n.id).some((e) => e.kind === 'calls' && e.target === n.id)) out.push(n.name);
}
}
return out.sort();
}
describe('a super call is not a self-call', () => {
it('Swift, Kotlin, Java, Python, TypeScript and C#', async () => {
const cg = await project({
'ios/CameraView.swift': `import UIKit
class CameraView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
setup()
}
func setup() {}
func countdown(_ n: Int) {
if n > 0 { countdown(n - 1) }
}
}
`,
'android/CameraView.kt': `package app
class CameraView(context: Context) : FrameLayout(context) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
}
}
`,
'java/Service.java': `package app;
public class Service extends Base {
@Override
public void start() {
super.start();
}
}
`,
'py/views.py': `class ProfileView(BaseView):
def dispatch(self, request):
return super().dispatch(request)
`,
'ts/list.ts': `export class List extends Base {
render(): string {
return super.render() + '!';
}
}
`,
'cs/Handler.cs': `public class Handler : BaseHandler {
public override void Handle() {
base.Handle();
}
}
`,
});
try {
// Only the real recursion is left.
expect(selfCalling(cg)).toEqual(['countdown']);
} finally {
cg.close();
}
});
});