Files
codegraph/__tests__/php-receiver-guess.test.ts
Colby Mchenry e6527a8d34 fix(php): an untyped receiver keeps a unique method only when named for it (#2152)
PHP joins the untyped-receiver languages for the unique-name method guess:
`$x->m()` takes the one project method named `m` only when the receiver
shares a word with its owner, or names a class (the whole name, or its last
camel word: `$newRole` → Role) whose ancestry — extends + trait `use`,
namespace-aware — has the method.

A/B (edges removed / added): guzzle -359, BookStack -302, console -124,
Slim -124, monica -53. Removals are PSR-7 / framework / exception calls
bound to a test double or unrelated helper; the deliberate loss is calls
through receivers named after neither (`$loaner->loansAsLoaner()`).

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

90 lines
2.4 KiB
TypeScript

/**
* A PHP call through a receiver nothing typed takes the one project method of
* that name only when the receiver is named after its class — or after a
* class that inherits it (`$page->save()` → Page extends Entity). Guzzle's
* tests' PSR-7 `$response->getHeaderLine()` went to a test double's method,
* symfony/console's `$e->getMessage()` to a progress bar's.
*/
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-php-receiver-'));
const files: Record<string, string> = {
'src/Models/Entity.php': `<?php
namespace App\\Models;
abstract class Entity
{
public function save(): bool { return true; }
}
`,
'src/Models/Page.php': `<?php
namespace App\\Models;
class Page extends Entity
{
}
`,
'src/Helper/ProgressBar.php': `<?php
namespace App\\Helper;
class ProgressBar
{
public function getMessage(): string { return ''; }
}
`,
'src/Uploads/UserAvatars.php': `<?php
namespace App\\Uploads;
class UserAvatars
{
public function assignToUser($user): void {}
}
`,
'src/Controller.php': `<?php
namespace App;
function handle($page, $avatars, $user)
{
try {
$page->save();
$avatars->assignToUser($user);
} catch (\\Exception $e) {
return $e->getMessage();
}
}
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('PHP calls through an untyped receiver', () => {
it('keep a receiver named after the class or one that inherits the method; drop the rest', () => {
const ids = cg.getNodesInFile('src/Controller.php').map((n) => n.id);
const targets = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
expect(targets).toContain('App\\Models::Entity::save');
expect(targets).toContain('App\\Uploads::UserAvatars::assignToUser');
expect(targets).not.toContain('App\\Helper::ProgressBar::getMessage');
});
});