mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* test(ingestion): characterize Laravel route → controller CALLS edges (RING4-2 #943) Pins the current processRoutesFromExtracted edge-emission behavior (which had no direct coverage) before migrating it off the legacy ResolutionContext.resolve tiered lookup. Locks edge target, reason, and confidence values. * refactor(ingestion): resolve Laravel route controllers via type registry (RING4-2 #943) Migrate processRoutesFromExtracted off the legacy ResolutionContext.resolve tiered lookup onto model.types.lookupClassByName (global class resolution) + model.symbols.lookupExactAll (same-file method lookup). Drops the TIER_CONFIDENCE dependency for a fixed ROUTE_EDGE_CONFIDENCE constant matching the prior global-tier confidence. Characterization tests (6) stay green — behavior preserved. * refactor(ingestion): delete ResolutionContext.resolve tiered lookup (RING4-2 #943) Removes the legacy tiered name resolution — resolve/resolveUncached, TieredCandidates, ResolutionTier, TIER_CONFIDENCE, walkBindingChain, the package-dir index, the per-file resolve cache, and tier-hit stats. The context is now a thin holder for the live SemanticModel plus the (now-dead) per-file import maps, which the follow-up prune removes. Deletes the dedicated resolution-context.test.ts and symbol-resolver.test.ts (both exercised the removed .resolve tiered lookup). Full unit suite green (the 3 analyze worker-pool tests are pre-existing load flakes — pass isolated). * refactor(ingestion): delete legacy import-map plumbing + wildcard synthesis (RING4-2 #943) The per-file importMap / namedImportMap / packageMap / moduleAliasMap that fed the retired tiered resolver are now dead — nothing reads them (IMPORTS edges come from scope-resolution's imports-to-edges bridge, independent of these maps). Removes: - wildcard-synthesis.ts (synthesized the dead namedImportMap/moduleAliasMap) - import-processor's resolution path (processImports/processImportsFromExtracted/ wireImplicitImports/buildImportResolutionContext), keeping only the live preprocessImportPath path-cleanup helper - the parse-impl orchestration that drove them The parse phase now threads its SemanticModel to scope-resolution directly (parseOutput.model) instead of wrapping it in the resolution context. Deletes the obsolete wildcard/import-processor unit tests; trims the dead processImports cases from sequential-language-availability (processParsing coverage kept). * refactor(ingestion): delete resolution context + named-binding plumbing (RING4-2 #943) Completes the legacy-resolution retirement. With the tiered resolver gone, the entire per-file import-extraction chain is dead — its only consumer was the deleted ResolutionContext.resolve, and scope-resolution emits IMPORTS edges from its own finalized ImportEdges: - delete model/resolution-context.ts (the legacy context); the parse phase now hands its SemanticModel to scope-resolution as parseOutput.model - delete the named-bindings/ extractors + the namedBindingExtractor provider hook (built the dead NamedImportMap) across all 8 providers + the worker - delete the orphaned implicitImportWirer hook + Swift implementation + providersWithImplicitWiring (scope-resolution owns implicit imports now) - drop the dead ExtractedImport type + worker/sequential import accumulation (result.imports / WorkerExtractedData.imports) - import-processor.ts and its preprocessImportPath helper are now unreferenced Deletes the obsolete named-bindings + preprocessImportPath unit tests. tsc clean; full unit suite green (3 analyze worker-pool tests are pre-existing load flakes); 1229 import/cross-file/resolver integration tests pass incl. the wildcard-import languages (Go/Ruby/C++/Swift) that previously used synthesis. * docs(ingestion): scrub stale references to deleted resolution-context machinery (RING4-2 #943) * docs(ingestion): reword route resolver comment to clear acceptance grep gate (#943) * fix(review): apply autofix feedback (RING4-2 #943) Code-review autofixes from the multi-agent pass: - delete orphaned dead code the deletion missed: swift.ts groupSwiftFilesByTarget + SwiftPackageConfig import (live copy is target-grouping.ts), import-resolvers EMPTY_INDEX export (no consumers after the importCtx reset was removed) - scrub stale comments referencing deleted symbols (processImports, preprocessImportPath, moduleAliasMap, NamedImportMap/PackageMap, wildcard-synthesis) and fix a broken comment fragment in parse-impl.ts - document the intentional global-resolution convergence for route controllers (the import-scoped tier was deleted with the resolver): confidence flattens 0.9→0.5 but resolved edges stay at the 0.5 process-trace/community gate; only the narrow imported-controller-with-unresolved-method guessed edge crosses it - add an overloaded-method characterization case pinning lookupExactAll[0] * style(ingestion): prettier-format parse-impl unwind + route characterization test (#943) * refactor(ingestion): address tri-review findings (RING4-2 #943) From the PR #2033 tri-review (Codex + CE lanes): - delete the now-dead importSemantics provider field + ImportSemantics type (wildcard-synthesis.ts was its sole consumer; zero readers remain) across language-provider.ts + 7 providers + DEFAULTS - correct the processRoutesFromExtracted JSDoc: the import-disambiguated controller skip is STRICTER than the legacy global-tier guard (the legacy import-scoped tier resolved aliased / same-short-name controllers and emitted the edge); document the aliased-import missed-edge case explicitly - add an aliased-controller characterization test pinning the documented global-resolution convergence (no edge for an aliased/unresolvable controller name) - scrub stale parse-impl.ts docstrings/comments that still listed the removed import-resolution / wildcard-synthesis / heritage passes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ingestion): capture routes-file use/FQN map for Laravel controller resolution (#943) Adds ExtractedRoute.controllerQualifiedName: the Laravel route extractor now builds the routes file's `use`-import alias map (local→normalized dot-joined FQN, via splitNamespaceUseDeclaration) and captures inline qualified ::class references, threading the disambiguating FQN through every route. Normalized via the shared normalizeQualifiedName so it matches the type registry's key shape (issue #1982). Foundation for qualified-first route→controller resolution (U2). * fix(ingestion): resolve Laravel route controllers qualified-first (#943) processRoutesFromExtracted now resolves the controller via model.types.lookupClassByQualifiedName(route.controllerQualifiedName) when the extractor disambiguated it (aliased use / same-short-name / inline FQN), falling back to the short-name lookupClassByName (which still skips on ambiguity). This restores the route→controller CALLS edges the PR #2033 tri-review (Codex F1 + ce-adversarial) found dropped, without re-adding the deleted per-file import map. Method resolution, guessed-id, and confidence are unchanged. JSDoc rewritten to qualified-first precedence; the aliased characterization test flips from no-edge to edge; adds duplicated-name-disambiguated + stale-FQN-fallback cases. * test(ingestion): end-to-end Laravel route→controller qualified resolution + PSR-4 disambiguation (#943) Adds an integration test that parses real namespaced PHP controllers + a routes file through the worker pipeline and asserts the route CALLS edges target the correct namespaced controller — the authoritative gate the unit tests can't be (hand-built models). It surfaced that PHP's statement-form `namespace X;` leaves the structure-phase qualifiedName as the SHORT name, so lookupClassByQualifiedName misses; resolveControllerByQualifiedName now adds a PSR-4 file-path disambiguation (FQN namespace tail ↔ file directory tail) to pick the right same-short-name controller. Forces the worker path (workerThresholdsForTest) since route extraction is worker-only. * style(ingestion): prettier-format Laravel route resolution changes (#943) * test(ingestion): regenerate php-captures golden for the new php-laravel-routes fixture (#943) * test(ingestion): move route fixture out of the php-* scope-capture corpus (#943) The laravel route-resolution fixture lived under lang-resolution/php-laravel-routes, which the php scope-capture golden + benchmark both glob (lang-resolution/php-*), drifting their fingerprints. The fixture is for route resolution, not php scope-capture parity, so rename it to lang-resolution/laravel-route-resolution to decouple it. Reverts the golden's php-laravel-routes entries; bench scope-capture --check passes (php back to baseline). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
283 lines
8.5 KiB
TypeScript
283 lines
8.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import PHP from 'tree-sitter-php';
|
|
import { extractLaravelRoutes } from '../../src/core/ingestion/route-extractors/laravel.js';
|
|
|
|
const parser = new Parser();
|
|
parser.setLanguage(PHP.php_only);
|
|
|
|
const extract = (source: string) =>
|
|
extractLaravelRoutes(parser.parse(source), 'routes/web.php').map((route) => ({
|
|
httpMethod: route.httpMethod,
|
|
routePath: route.routePath,
|
|
controllerName: route.controllerName,
|
|
methodName: route.methodName,
|
|
routeName: route.routeName,
|
|
middleware: route.middleware,
|
|
prefix: route.prefix,
|
|
}));
|
|
|
|
describe('Laravel route extraction', () => {
|
|
it('extracts representative HTTP verb route declarations', () => {
|
|
const routes = extract(`<?php
|
|
Route::get('/orders', [OrderController::class, 'index']);
|
|
Route::post('/orders', [OrderController::class, 'store']);
|
|
Route::put('/orders/{order}', [OrderController::class, 'update']);
|
|
Route::patch('/orders/{order}', [OrderController::class, 'patch']);
|
|
Route::delete('/orders/{order}', [OrderController::class, 'destroy']);
|
|
Route::options('/orders/options', [OrderController::class, 'options']);
|
|
Route::any('/orders/any', [OrderController::class, 'any']);
|
|
Route::match(['get', 'post'], '/orders/search', [OrderController::class, 'search']);
|
|
`);
|
|
|
|
expect(routes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ httpMethod: 'get', routePath: '/orders', methodName: 'index' }),
|
|
expect.objectContaining({ httpMethod: 'post', routePath: '/orders', methodName: 'store' }),
|
|
expect.objectContaining({
|
|
httpMethod: 'put',
|
|
routePath: '/orders/{order}',
|
|
methodName: 'update',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'patch',
|
|
routePath: '/orders/{order}',
|
|
methodName: 'patch',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'delete',
|
|
routePath: '/orders/{order}',
|
|
methodName: 'destroy',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'options',
|
|
routePath: '/orders/options',
|
|
methodName: 'options',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'any',
|
|
routePath: '/orders/any',
|
|
methodName: 'any',
|
|
}),
|
|
expect.objectContaining({ httpMethod: 'match', routePath: '/orders/search' }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('expands resource and apiResource controller actions', () => {
|
|
const routes = extract(`<?php
|
|
Route::resource('/photos', PhotoController::class);
|
|
Route::apiResource('/api/photos', ApiPhotoController::class);
|
|
`);
|
|
|
|
const photos = routes.filter((route) => route.routePath === '/photos');
|
|
expect(photos.map((route) => route.methodName)).toEqual([
|
|
'index',
|
|
'create',
|
|
'store',
|
|
'show',
|
|
'edit',
|
|
'update',
|
|
'destroy',
|
|
]);
|
|
expect(new Set(photos.map((route) => route.controllerName))).toEqual(
|
|
new Set(['PhotoController']),
|
|
);
|
|
expect(photos.map((route) => route.routeName)).toEqual([
|
|
'photos.index',
|
|
'photos.create',
|
|
'photos.store',
|
|
'photos.show',
|
|
'photos.edit',
|
|
'photos.update',
|
|
'photos.destroy',
|
|
]);
|
|
|
|
const apiPhotos = routes.filter((route) => route.routePath === '/api/photos');
|
|
expect(apiPhotos.map((route) => route.methodName)).toEqual([
|
|
'index',
|
|
'store',
|
|
'show',
|
|
'update',
|
|
'destroy',
|
|
]);
|
|
expect(new Set(apiPhotos.map((route) => route.controllerName))).toEqual(
|
|
new Set(['ApiPhotoController']),
|
|
);
|
|
expect(apiPhotos.map((route) => route.routeName)).toEqual([
|
|
'api.photos.index',
|
|
'api.photos.store',
|
|
'api.photos.show',
|
|
'api.photos.update',
|
|
'api.photos.destroy',
|
|
]);
|
|
});
|
|
|
|
it('threads middleware, prefix, and controller chains into grouped routes', () => {
|
|
const routes = extract(`<?php
|
|
Route::get('/loose', 'index');
|
|
|
|
Route::group([
|
|
'prefix' => 'api',
|
|
'middleware' => ['auth'],
|
|
'controller' => ApiOrderController::class,
|
|
], function () {
|
|
Route::post('/orders', 'store');
|
|
});
|
|
|
|
Route::middleware(['auth', 'verified'])
|
|
->prefix('admin')
|
|
->controller(OrderController::class)
|
|
->group(function () {
|
|
Route::get('/orders', 'index');
|
|
});
|
|
`);
|
|
|
|
expect(routes).toContainEqual(
|
|
expect.objectContaining({
|
|
httpMethod: 'get',
|
|
routePath: '/orders',
|
|
controllerName: 'OrderController',
|
|
methodName: 'index',
|
|
middleware: ['auth', 'verified'],
|
|
prefix: 'admin',
|
|
}),
|
|
);
|
|
|
|
expect(routes).toContainEqual(
|
|
expect.objectContaining({
|
|
httpMethod: 'post',
|
|
routePath: '/orders',
|
|
controllerName: 'ApiOrderController',
|
|
methodName: 'store',
|
|
middleware: ['auth'],
|
|
prefix: 'api',
|
|
}),
|
|
);
|
|
|
|
expect(routes).toContainEqual(
|
|
expect.objectContaining({
|
|
httpMethod: 'get',
|
|
routePath: '/loose',
|
|
controllerName: null,
|
|
methodName: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('extracts named routes from fluent routes and named groups', () => {
|
|
const routes = extract(`<?php
|
|
Route::get('/login', [AuthController::class, 'login'])->name('login');
|
|
Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth')->name('logout');
|
|
|
|
Route::name('admin.')
|
|
->prefix('admin')
|
|
->group(function () {
|
|
Route::post('/settings/cache', [SettingsController::class, 'refresh'])
|
|
->name('settings.refresh-cache');
|
|
});
|
|
|
|
Route::group([
|
|
'as' => 'log-viewer.',
|
|
'prefix' => 'logs',
|
|
], function () {
|
|
Route::post('/login', [LogViewerController::class, 'login'])
|
|
->name('login.submit');
|
|
});
|
|
`);
|
|
|
|
expect(routes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
httpMethod: 'get',
|
|
routePath: '/login',
|
|
routeName: 'login',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'post',
|
|
routePath: '/logout',
|
|
routeName: 'logout',
|
|
middleware: ['auth'],
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'post',
|
|
routePath: '/settings/cache',
|
|
routeName: 'admin.settings.refresh-cache',
|
|
prefix: 'admin',
|
|
}),
|
|
expect.objectContaining({
|
|
httpMethod: 'post',
|
|
routePath: '/login',
|
|
routeName: 'log-viewer.login.submit',
|
|
prefix: 'logs',
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Laravel controller qualified-name resolution (RING4-2 follow-up)', () => {
|
|
const routeFor = (source: string, routePath: string) =>
|
|
extractLaravelRoutes(parser.parse(source), 'routes/web.php').find(
|
|
(r) => r.routePath === routePath,
|
|
);
|
|
|
|
it('resolves an aliased controller import to its normalized FQN', () => {
|
|
const route = routeFor(
|
|
`<?php
|
|
use App\\Http\\Controllers\\OrderController as Orders;
|
|
Route::get('/orders', [Orders::class, 'index']);
|
|
`,
|
|
'/orders',
|
|
);
|
|
expect(route?.controllerName).toBe('Orders');
|
|
expect(route?.controllerQualifiedName).toBe('App.Http.Controllers.OrderController');
|
|
});
|
|
|
|
it('resolves a plain (non-aliased) controller import to its normalized FQN', () => {
|
|
const route = routeFor(
|
|
`<?php
|
|
use App\\Http\\Controllers\\OrderController;
|
|
Route::get('/orders', [OrderController::class, 'index']);
|
|
`,
|
|
'/orders',
|
|
);
|
|
expect(route?.controllerName).toBe('OrderController');
|
|
expect(route?.controllerQualifiedName).toBe('App.Http.Controllers.OrderController');
|
|
});
|
|
|
|
it('captures an inline qualified ::class reference as the normalized FQN', () => {
|
|
const route = routeFor(
|
|
`<?php
|
|
Route::get('/orders', [\\App\\Admin\\OrderController::class, 'index']);
|
|
`,
|
|
'/orders',
|
|
);
|
|
expect(route?.controllerQualifiedName).toBe('App.Admin.OrderController');
|
|
});
|
|
|
|
it('leaves controllerQualifiedName null for a bare short name with no use import', () => {
|
|
const route = routeFor(
|
|
`<?php
|
|
Route::get('/orders', [OrderController::class, 'index']);
|
|
`,
|
|
'/orders',
|
|
);
|
|
expect(route?.controllerName).toBe('OrderController');
|
|
expect(route?.controllerQualifiedName ?? null).toBeNull();
|
|
});
|
|
|
|
it('threads the FQN through resource routes', () => {
|
|
const routes = extractLaravelRoutes(
|
|
parser.parse(`<?php
|
|
use App\\Http\\Controllers\\PhotoController as Photos;
|
|
Route::resource('/photos', Photos::class);
|
|
`),
|
|
'routes/web.php',
|
|
).filter((r) => r.routePath === '/photos');
|
|
expect(routes.length).toBeGreaterThan(0);
|
|
for (const r of routes) {
|
|
expect(r.controllerQualifiedName).toBe('App.Http.Controllers.PhotoController');
|
|
}
|
|
});
|
|
});
|