GitNexus/gitnexus/test/unit/framework-detection.test.ts
Gergo Magyar 76ed0fa53b fix: address PR #409 review findings (P0-P3) and simplify import resolution API
Bug fixes (P0):
- Narrow Go /cmd/ entry-point detection to only match /main.go
- Fix Rust scoped grouped imports (use crate::models::{User, Repo}) resolution
- Filter PHP use function/use const from class-type namedImportMap bindings

Improvements (P1):
- Add C# resolveStandard fallback when .csproj discovery fails
- Change preprocessImportPath return type to string | null with caller guards
- Add Q_SIGNALS/Q_SLOTS (standard plural Qt macros)
- Fix stale "11 supported languages" comment → 13

API simplification (P2):
- Replace buildImportResolvers() factory with const importResolvers table
- Move configs onto ResolveCtx (extends ImportResolutionContext)
- Eliminate tsconfigPaths parameter threading through 6 non-TS resolvers
- Split utils.ts (1,476 lines) into ast-helpers.ts + call-analysis.ts + utils.ts
- Consolidate findChild/findChildByType into single source of truth
- Match multi-file import bindings to files by basename for namedImportMap

Cleanup (P3):
- EMPTY_INDEX returns shared frozen empty array instead of allocating per call
- Type appendKotlinWildcard parameter as SyntaxNode instead of any
- Document call-routing validation requirement on CallRouter type

Tests:
- Add unit tests for preprocessImportPath (13 tests)
- Add integration tests: Rust scoped multi-file, PHP use function/const,
  C# without .csproj, Go cmd/ helper scoring (14 tests, 4 fixtures)

All 3579 tests pass.
2026-03-21 14:15:15 +00:00

336 lines
12 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { detectFrameworkFromPath, detectFrameworkFromAST, FRAMEWORK_AST_PATTERNS } from '../../src/core/ingestion/framework-detection.js';
describe('detectFrameworkFromPath', () => {
describe('Next.js', () => {
it('detects Pages Router pages', () => {
const result = detectFrameworkFromPath('pages/users.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-pages');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('ignores _app and _document pages', () => {
expect(detectFrameworkFromPath('pages/_app.tsx')).toBeNull();
});
it('detects App Router page.tsx', () => {
const result = detectFrameworkFromPath('app/dashboard/page.tsx');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-app');
});
it('detects API routes in pages', () => {
const result = detectFrameworkFromPath('pages/api/users.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-api');
});
it('detects App Router API route.ts', () => {
const result = detectFrameworkFromPath('app/api/users/route.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nextjs-api');
});
it('detects layout files', () => {
const result = detectFrameworkFromPath('app/layout.tsx');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(2.0);
});
});
describe('Express / Node.js', () => {
it('detects route files', () => {
const result = detectFrameworkFromPath('routes/auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('express');
expect(result!.entryPointMultiplier).toBe(2.5);
});
});
describe('MVC controllers', () => {
it('detects controller folder', () => {
const result = detectFrameworkFromPath('controllers/UserController.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('mvc');
});
it('detects handlers folder', () => {
const result = detectFrameworkFromPath('handlers/auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('handlers');
});
});
describe('React', () => {
it('has React component detection rule for views/components folders', () => {
// Note: The current implementation lowercases the path before checking
// PascalCase, so PascalCase detection currently can't match.
// This test documents the current behavior.
const result = detectFrameworkFromPath('views/Button.tsx');
// Returns null because path is lowercased before PascalCase regex check
expect(result).toBeNull();
});
});
describe('Python frameworks', () => {
it('detects Django views', () => {
const result = detectFrameworkFromPath('myapp/views.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('django');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects Django URLs', () => {
const result = detectFrameworkFromPath('myapp/urls.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('django');
});
it('detects FastAPI routers', () => {
const result = detectFrameworkFromPath('routers/users.py');
expect(result).not.toBeNull();
expect(result!.framework).toBe('fastapi');
});
});
describe('Java frameworks', () => {
it('detects Spring controllers folder', () => {
const result = detectFrameworkFromPath('controller/UserController.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects Spring controller by filename', () => {
const result = detectFrameworkFromPath('src/UserController.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects Java service layer', () => {
const result = detectFrameworkFromPath('service/UserService.java');
expect(result).not.toBeNull();
expect(result!.framework).toBe('java-service');
});
});
describe('C# / .NET', () => {
it('detects ASP.NET controllers', () => {
const result = detectFrameworkFromPath('controllers/UsersController.cs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('aspnet');
});
it('detects Blazor pages', () => {
const result = detectFrameworkFromPath('pages/Index.razor');
expect(result).not.toBeNull();
expect(result!.framework).toBe('blazor');
});
});
describe('Go frameworks', () => {
it('detects Go handlers', () => {
const result = detectFrameworkFromPath('handlers/user.go');
expect(result).not.toBeNull();
expect(result!.framework).toBe('go-http');
});
it('detects Go main.go', () => {
const result = detectFrameworkFromPath('cmd/server/main.go');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('does NOT treat Go helper files under cmd/ as entry points', () => {
expect(detectFrameworkFromPath('cmd/server/internal/util.go')).toBeNull();
expect(detectFrameworkFromPath('cmd/foo/config/setup.go')).toBeNull();
});
});
describe('Rust frameworks', () => {
it('detects Rust handlers', () => {
const result = detectFrameworkFromPath('handlers/auth.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust-web');
});
it('detects main.rs', () => {
const result = detectFrameworkFromPath('src/main.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects bin folder', () => {
const result = detectFrameworkFromPath('src/bin/cli.rs');
expect(result).not.toBeNull();
expect(result!.framework).toBe('rust');
});
});
describe('C / C++', () => {
it('detects main.c', () => {
const result = detectFrameworkFromPath('src/main.c');
expect(result).not.toBeNull();
expect(result!.framework).toBe('c-cpp');
});
it('detects main.cpp', () => {
const result = detectFrameworkFromPath('src/main.cpp');
expect(result).not.toBeNull();
expect(result!.framework).toBe('c-cpp');
});
});
describe('PHP / Laravel', () => {
it('detects Laravel routes', () => {
const result = detectFrameworkFromPath('routes/web.php');
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
expect(result!.entryPointMultiplier).toBe(3.0);
});
it('detects Laravel controllers', () => {
const result = detectFrameworkFromPath('http/controllers/UserController.php');
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
});
it('detects Laravel jobs', () => {
const result = detectFrameworkFromPath('jobs/SendEmail.php');
expect(result).not.toBeNull();
expect(result!.reason).toBe('laravel-job');
});
it('detects Laravel middleware', () => {
const result = detectFrameworkFromPath('http/middleware/Auth.php');
expect(result).not.toBeNull();
expect(result!.reason).toBe('laravel-middleware');
});
it('detects Laravel models', () => {
const result = detectFrameworkFromPath('models/User.php');
expect(result).not.toBeNull();
expect(result!.entryPointMultiplier).toBe(1.5);
});
});
describe('Swift / iOS', () => {
it('detects AppDelegate', () => {
const result = detectFrameworkFromPath('Sources/AppDelegate.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('ios');
});
it('detects ViewControllers folder', () => {
const result = detectFrameworkFromPath('ViewControllers/LoginVC.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('uikit');
});
it('detects Coordinator pattern', () => {
const result = detectFrameworkFromPath('Coordinators/AppCoordinator.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('ios-coordinator');
});
it('detects SwiftUI views folder', () => {
const result = detectFrameworkFromPath('views/ContentView.swift');
expect(result).not.toBeNull();
expect(result!.framework).toBe('swiftui');
});
});
describe('generic patterns', () => {
it('returns null for unknown paths', () => {
expect(detectFrameworkFromPath('src/internal/crypto.ts')).toBeNull();
});
it('normalizes Windows backslashes', () => {
const result = detectFrameworkFromPath('routes\\auth.ts');
expect(result).not.toBeNull();
expect(result!.framework).toBe('express');
});
});
});
describe('detectFrameworkFromAST', () => {
it('returns null for empty inputs', () => {
expect(detectFrameworkFromAST('', '')).toBeNull();
expect(detectFrameworkFromAST('typescript', '')).toBeNull();
expect(detectFrameworkFromAST('', 'some code')).toBeNull();
});
it('detects NestJS decorators in TypeScript', () => {
const result = detectFrameworkFromAST('typescript', '@Controller("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
expect(result!.entryPointMultiplier).toBe(3.2);
});
it('detects NestJS decorators in JavaScript', () => {
const result = detectFrameworkFromAST('javascript', '@Get("/")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('nestjs');
});
it('detects FastAPI decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.get("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('fastapi');
});
it('detects Flask decorators in Python', () => {
const result = detectFrameworkFromAST('python', '@app.route("/users")');
expect(result).not.toBeNull();
expect(result!.framework).toBe('flask');
});
it('detects Spring annotations in Java', () => {
const result = detectFrameworkFromAST('java', '@RestController');
expect(result).not.toBeNull();
expect(result!.framework).toBe('spring');
});
it('detects ASP.NET attributes in C#', () => {
const result = detectFrameworkFromAST('csharp', '[ApiController]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('aspnet');
});
it('detects Laravel route definitions in PHP', () => {
const result = detectFrameworkFromAST('php', "Route::get('/users', [UserController::class, 'index'])");
expect(result).not.toBeNull();
expect(result!.framework).toBe('laravel');
});
it('detects Actix-web route attributes in Rust', () => {
const result = detectFrameworkFromAST('rust', '#[get("/")]');
expect(result).not.toBeNull();
expect(result!.framework).toBe('actix-web');
});
it('returns null for language with no matching pattern', () => {
expect(detectFrameworkFromAST('c', 'int main() { return 0; }')).toBeNull();
});
it('is case-insensitive', () => {
const result = detectFrameworkFromAST('TypeScript', '@controller("/")');
expect(result).not.toBeNull();
});
});
describe('FRAMEWORK_AST_PATTERNS', () => {
it('has patterns for all expected frameworks', () => {
const expectedFrameworks = [
'nestjs', 'express', 'fastapi', 'flask', 'spring', 'jaxrs',
'aspnet', 'go-http', 'gin', 'echo', 'fiber', 'go-grpc',
'laravel', 'actix', 'axum', 'rocket', 'tokio', 'qt',
'uikit', 'swiftui', 'vapor', 'rails', 'sinatra',
];
for (const fw of expectedFrameworks) {
expect(FRAMEWORK_AST_PATTERNS).toHaveProperty(fw);
expect(FRAMEWORK_AST_PATTERNS[fw as keyof typeof FRAMEWORK_AST_PATTERNS].length).toBeGreaterThan(0);
}
});
});