mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* refactor(ingestion): consolidate per-language patterns into LanguageProvider Move entry-point name patterns and AST framework detection patterns from shared maps in entry-point-scoring.ts and framework-detection.ts into each LanguageProvider. The shared files now build their lookup tables dynamically from the provider registry at module load. This aligns with the architecture principle that shared pipeline code must not name languages. Adding a new language no longer requires modifying entry-point-scoring.ts or framework-detection.ts — the provider file is the single source of truth for all language-specific data. New LanguageProvider fields: - entryPointPatterns: RegExp[] (default: []) - astFrameworkPatterns: AstFrameworkPatternConfig[] (default: []) * test(ingestion): add provider-registry, multiplier/reason, and Kotlin/Dart/Ruby entry-point coverage Addresses review feedback on the per-language pattern consolidation: - Runtime guard that providers map covers every SupportedLanguages member, catching enum/registry drift that the compile-time `satisfies` cannot. - Multiplier/reason parity assertions for nestjs (3.2/nestjs-decorator), spring (3.2/spring-annotation), and fastapi (3.0/fastapi-decorator) so a silent value change during future relocations would fail loudly. - Entry-point pattern coverage for Kotlin (Android lifecycle, ViewModel, Service), Dart (Flutter widget lifecycle), and Ruby (call/perform/execute) — the three providers whose patterns moved without representative tests. * refactor(ingestion): apply satisfies AstFrameworkPatternConfig[] to remaining providers The c-cpp, dart, php, ruby, and swift providers imported AstFrameworkPatternConfig but never used it, which the root ESLint config flagged as a hard error in the quality / lint CI gate. Use the type the same way csharp/go/java/kotlin/python/rust/typescript already do — as a satisfies assertion on the astFrameworkPatterns array. This both clears the unused-import error and gives every provider compile-time validation of pattern shape, narrowing the gap that the original review flagged about lost exhaustiveness on the optional astFrameworkPatterns field. --------- Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
import {
|
|
detectFrameworkFromPath,
|
|
detectFrameworkFromAST,
|
|
} from '../../src/core/ingestion/framework-detection.js';
|
|
import { providers } from '../../src/core/ingestion/languages/index.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('Expo Router', () => {
|
|
it('detects _layout files', () => {
|
|
const result = detectFrameworkFromPath('app/_layout.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-layout');
|
|
});
|
|
it('detects +api routes', () => {
|
|
const result = detectFrameworkFromPath('app/users+api.ts');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-api-route');
|
|
});
|
|
it('detects screen files', () => {
|
|
const result = detectFrameworkFromPath('app/(tabs)/settings.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
expect(result!.reason).toBe('expo-screen');
|
|
expect(result!.entryPointMultiplier).toBe(2.5);
|
|
});
|
|
});
|
|
|
|
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', () => {
|
|
const result = detectFrameworkFromPath('views/Button.tsx');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('react');
|
|
expect(result!.entryPointMultiplier).toBe(1.5);
|
|
expect(result!.reason).toBe('react-component');
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
it('detects Expo Router useRouter hook', () => {
|
|
const result = detectFrameworkFromAST('typescript', 'const router = useRouter()');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
});
|
|
it('detects Expo Router router.push', () => {
|
|
const result = detectFrameworkFromAST('javascript', "router.push('/settings')");
|
|
expect(result).not.toBeNull();
|
|
expect(result!.framework).toBe('expo-router');
|
|
});
|
|
});
|
|
|
|
describe('provider registry exhaustiveness', () => {
|
|
// Compile-time exhaustiveness is enforced by `satisfies Record<SupportedLanguages, ...>`
|
|
// in languages/index.ts. This runtime guard catches drift if the enum and the
|
|
// providers map ever diverge at runtime (e.g. via build-artifact mismatch),
|
|
// since both shared lookup maps are built from `Object.entries(providers)`.
|
|
it('has one provider per SupportedLanguages enum member', () => {
|
|
expect(Object.keys(providers).sort()).toEqual(Object.values(SupportedLanguages).sort());
|
|
});
|
|
});
|
|
|
|
describe('provider astFrameworkPatterns', () => {
|
|
it('preserves multiplier and reason for high-priority frameworks', () => {
|
|
// Pin the exact multiplier and reason strings to catch silent drift
|
|
// during the per-language pattern relocation. These three frameworks
|
|
// are the most-used decorator-driven entry-point boosters.
|
|
const expectedConfigs: Record<string, { multiplier: number; reason: string }> = {
|
|
nestjs: { multiplier: 3.2, reason: 'nestjs-decorator' },
|
|
spring: { multiplier: 3.2, reason: 'spring-annotation' },
|
|
fastapi: { multiplier: 3.0, reason: 'fastapi-decorator' },
|
|
};
|
|
const seen = new Set<string>();
|
|
for (const provider of Object.values(providers)) {
|
|
for (const cfg of provider.astFrameworkPatterns ?? []) {
|
|
const expected = expectedConfigs[cfg.framework];
|
|
if (!expected) continue;
|
|
expect(cfg.entryPointMultiplier).toBe(expected.multiplier);
|
|
expect(cfg.reason).toBe(expected.reason);
|
|
seen.add(cfg.framework);
|
|
}
|
|
}
|
|
for (const fw of Object.keys(expectedConfigs)) {
|
|
expect(seen.has(fw), `missing framework config: ${fw}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('covers all expected frameworks across providers', () => {
|
|
const expectedFrameworks = new Set([
|
|
'nestjs',
|
|
'expo-router',
|
|
'fastapi',
|
|
'flask',
|
|
'spring',
|
|
'jaxrs',
|
|
'aspnet',
|
|
'signalr',
|
|
'blazor',
|
|
'efcore',
|
|
'go-http',
|
|
'gin',
|
|
'echo',
|
|
'fiber',
|
|
'go-grpc',
|
|
'laravel',
|
|
'actix-web',
|
|
'axum',
|
|
'rocket',
|
|
'tokio',
|
|
'qt',
|
|
'uikit',
|
|
'swiftui',
|
|
'vapor',
|
|
'rails',
|
|
'sinatra',
|
|
'spring-kotlin',
|
|
'ktor',
|
|
'android-kotlin',
|
|
'flutter',
|
|
'riverpod',
|
|
]);
|
|
const foundFrameworks = new Set<string>();
|
|
for (const provider of Object.values(providers)) {
|
|
for (const cfg of provider.astFrameworkPatterns ?? []) {
|
|
foundFrameworks.add(cfg.framework);
|
|
expect(cfg.patterns.length).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
for (const fw of expectedFrameworks) {
|
|
expect(foundFrameworks.has(fw), `missing framework: ${fw}`).toBe(true);
|
|
}
|
|
});
|
|
});
|