mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
104b8de3e6
Only `XController@action` was claimed and resolved, and the written namespace was stripped, so akaunting's 'Common\\Uploads@inline' (and a Route::resource with a string controller plus options) bound 304 of 313 routes to nothing. Any `Class@action` is claimed; the namespace path under App\\Http\\Controllers is kept and picks between same-named controllers; a resource reads its first argument and resolves its controller class in the Laravel resolver (no `use` brings a string-named class into scope). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
/**
|
|
* A Laravel route names its controller however the app writes it: akaunting
|
|
* writes `'Common\Uploads@inline'` — no `Controller` suffix, a namespace below
|
|
* App\Http\Controllers — and `Route::resource('companies', 'Common\Companies',
|
|
* [...options])`. Only `XController@action` was read, so 304 of akaunting's
|
|
* 313 routes bound to nothing. Same-named controllers in two namespaces are
|
|
* told apart by the written path.
|
|
*/
|
|
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;
|
|
|
|
const controller = (ns: string, name: string, methods: string[]) => `<?php
|
|
|
|
namespace App\\Http\\Controllers\\${ns};
|
|
|
|
class ${name}
|
|
{
|
|
${methods.map((m) => ` public function ${m}() { return 1; }`).join('\n')}
|
|
}
|
|
`;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-laravel-strings-'));
|
|
const files: Record<string, string> = {
|
|
'artisan': '#!/usr/bin/env php\n',
|
|
'composer.json': JSON.stringify({ require: { 'laravel/framework': '^10.0' } }),
|
|
'routes/admin.php': `<?php
|
|
|
|
use Illuminate\\Support\\Facades\\Route;
|
|
|
|
Route::get('uploads/{id}/inline', 'Common\\Uploads@inline')->name('inline');
|
|
Route::delete('uploads/{id}', 'Common\\Uploads@destroy');
|
|
Route::get('portal/uploads/{id}', 'Portal\\Uploads@inline');
|
|
Route::resource('companies', 'Common\\Companies', ['middleware' => ['dropzone']]);
|
|
`,
|
|
'app/Http/Controllers/Common/Uploads.php': controller('Common', 'Uploads', ['inline', 'destroy']),
|
|
'app/Http/Controllers/Portal/Uploads.php': controller('Portal', 'Uploads', ['inline']),
|
|
'app/Http/Controllers/Common/Companies.php': controller('Common', 'Companies', ['index', 'store']),
|
|
};
|
|
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 });
|
|
});
|
|
|
|
const servedBy = (routeName: string) => {
|
|
const route = cg.getNodesByKind('route').find((r) => r.name === routeName);
|
|
expect(route, routeName).toBeDefined();
|
|
return cg.getOutgoingEdgesFrom([route!.id]).map((e) => cg.getNode(e.target)!).map((n) => `${n.name} ${n.filePath}`);
|
|
};
|
|
|
|
describe('Laravel string controllers', () => {
|
|
it('resolve without a Controller suffix, by their namespace path', () => {
|
|
expect(servedBy('GET /uploads/{id}/inline')).toEqual(['inline app/Http/Controllers/Common/Uploads.php']);
|
|
expect(servedBy('DELETE /uploads/{id}')).toEqual(['destroy app/Http/Controllers/Common/Uploads.php']);
|
|
expect(servedBy('GET /portal/uploads/{id}')).toEqual(['inline app/Http/Controllers/Portal/Uploads.php']);
|
|
});
|
|
|
|
it('read a resource controller named by string, past its options', () => {
|
|
expect(servedBy('resource:companies')).toEqual(['Companies app/Http/Controllers/Common/Companies.php']);
|
|
});
|
|
});
|