Merge branch 'pr-64'

This commit is contained in:
abhigyanpatwari 2026-02-26 17:38:05 +05:30
commit e868fd6cf9
20 changed files with 667 additions and 16 deletions

Binary file not shown.

View file

@ -8,7 +8,7 @@ export enum SupportedLanguages {
CSharp = 'csharp',
Go = 'go',
Rust = 'rust',
// PHP = 'php',
PHP = 'php',
// Ruby = 'ruby',
// Swift = 'swift',
}

View file

@ -102,6 +102,27 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
/^Run$/, // Run methods
/^Start$/, // Start methods
],
// PHP / Laravel
'php': [
/Controller$/, // UserController (class name convention)
/^handle$/, // Job::handle(), Listener::handle()
/^execute$/, // Command::execute()
/^boot$/, // ServiceProvider::boot()
/^register$/, // ServiceProvider::register()
/^__invoke$/, // Invokable controllers/actions
/^(index|show|store|update|destroy|create|edit)$/, // RESTful resource methods
/^(get|post|put|delete|patch)[A-Z]/, // Explicit HTTP method actions
/^run$/, // Command/Job run()
/^fire$/, // Event fire()
/^dispatch$/, // Dispatchable jobs
/Service$/, // UserService (Service layer)
/Repository$/, // UserRepository (Repository pattern)
/^find$/, // Repository::find()
/^findAll$/, // Repository::findAll()
/^save$/, // Repository::save()
/^delete$/, // Repository::delete()
],
};
// ============================================================================
@ -252,7 +273,12 @@ export function isTestFile(filePath: string): boolean {
p.includes('/tests/') ||
// C# test patterns
p.includes('.tests/') ||
p.includes('tests.cs')
p.includes('tests.cs') ||
// PHP/Laravel test patterns
p.endsWith('test.php') ||
p.endsWith('spec.php') ||
p.includes('/tests/feature/') ||
p.includes('/tests/unit/')
);
}

View file

@ -195,8 +195,70 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'c-cpp', entryPointMultiplier: 2.5, reason: 'c-app' };
}
// ========== PHP / LARAVEL FRAMEWORKS ==========
// Laravel routes (highest - these ARE the entry point definitions)
if (p.includes('/routes/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-routes' };
}
// Laravel controllers (very high - receive HTTP requests)
if ((p.includes('/http/controllers/') || p.includes('/controllers/')) && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-controller' };
}
// Laravel controller by file name convention
if (p.endsWith('controller.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-controller-file' };
}
// Laravel console commands
if ((p.includes('/console/commands/') || p.includes('/commands/')) && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-command' };
}
// Laravel jobs (queue entry points)
if (p.includes('/jobs/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-job' };
}
// Laravel listeners (event-driven entry points)
if (p.includes('/listeners/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-listener' };
}
// Laravel middleware
if (p.includes('/http/middleware/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-middleware' };
}
// Laravel service providers
if (p.includes('/providers/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.8, reason: 'laravel-provider' };
}
// Laravel policies
if (p.includes('/policies/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.0, reason: 'laravel-policy' };
}
// Laravel models (important but not entry points per se)
if (p.includes('/models/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-model' };
}
// Laravel services (Service Repository pattern)
if (p.includes('/services/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.8, reason: 'laravel-service' };
}
// Laravel repositories (Service Repository pattern)
if (p.includes('/repositories/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' };
}
// ========== GENERIC PATTERNS ==========
// Any language: index files in API folders
if (p.includes('/api/') && (
p.endsWith('/index.ts') || p.endsWith('/index.js') ||
@ -235,7 +297,11 @@ export const FRAMEWORK_AST_PATTERNS = {
// Go patterns (function signatures)
'go-http': ['http.Handler', 'http.HandlerFunc', 'ServeHTTP'],
// PHP/Laravel
'laravel': ['Route::get', 'Route::post', 'Route::put', 'Route::delete',
'Route::resource', 'Route::apiResource', '#[Route('],
// Rust macros
'actix': ['#[get', '#[post', '#[put', '#[delete'],
'axum': ['Router::new'],

View file

@ -317,6 +317,85 @@ export const RUST_QUERIES = `
(impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (type_identifier) @heritage.class) @heritage
`;
// PHP queries - works with tree-sitter-php (php_only grammar)
export const PHP_QUERIES = `
; ── Namespace ────────────────────────────────────────────────────────────────
(namespace_definition
name: (namespace_name) @name) @definition.namespace
; ── Classes ──────────────────────────────────────────────────────────────────
(class_declaration
name: (name) @name) @definition.class
; ── Interfaces ───────────────────────────────────────────────────────────────
(interface_declaration
name: (name) @name) @definition.interface
; ── Traits ───────────────────────────────────────────────────────────────────
(trait_declaration
name: (name) @name) @definition.trait
; ── Enums (PHP 8.1) ──────────────────────────────────────────────────────────
(enum_declaration
name: (name) @name) @definition.enum
; ── Top-level functions ───────────────────────────────────────────────────────
(function_definition
name: (name) @name) @definition.function
; ── Methods (including constructors) ─────────────────────────────────────────
(method_declaration
name: (name) @name) @definition.method
; ── Class properties (including Eloquent $fillable, $casts, etc.) ────────────
(property_declaration
(property_element
(variable_name
(name) @name))) @definition.property
; ── Imports: use statements ──────────────────────────────────────────────────
; Simple: use App\\Models\\User;
(namespace_use_declaration
(namespace_use_clause
(qualified_name) @import.source)) @import
; ── Function/method calls ────────────────────────────────────────────────────
; Regular function call: foo()
(function_call_expression
function: (name) @call.name) @call
; Method call: $obj->method()
(member_call_expression
name: (name) @call.name) @call
; Nullsafe method call: $obj?->method()
(nullsafe_member_call_expression
name: (name) @call.name) @call
; Static call: Foo::bar() (php_only uses scoped_call_expression)
(scoped_call_expression
name: (name) @call.name) @call
; ── Heritage: extends ────────────────────────────────────────────────────────
(class_declaration
name: (name) @heritage.class
(base_clause
[(name) (qualified_name)] @heritage.extends)) @heritage
; ── Heritage: implements ─────────────────────────────────────────────────────
(class_declaration
name: (name) @heritage.class
(class_interface_clause
[(name) (qualified_name)] @heritage.implements)) @heritage.impl
; ── Heritage: use trait (must capture enclosing class name) ──────────────────
(class_declaration
name: (name) @heritage.class
body: (declaration_list
(use_declaration
[(name) (qualified_name)] @heritage.trait))) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -327,5 +406,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
};

View file

@ -25,6 +25,12 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
if (filename.endsWith('.go')) return SupportedLanguages.Go;
// Rust
if (filename.endsWith('.rs')) return SupportedLanguages.Rust;
// PHP (all common extensions)
if (filename.endsWith('.php') || filename.endsWith('.phtml') ||
filename.endsWith('.php3') || filename.endsWith('.php4') ||
filename.endsWith('.php5') || filename.endsWith('.php8')) {
return SupportedLanguages.PHP;
}
return null;
};

View file

@ -39,6 +39,7 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string =>
[SupportedLanguages.CSharp]: '/wasm/csharp/tree-sitter-csharp.wasm',
[SupportedLanguages.Go]: '/wasm/go/tree-sitter-go.wasm',
[SupportedLanguages.Rust]: '/wasm/rust/tree-sitter-rust.wasm',
[SupportedLanguages.PHP]: '/wasm/php/tree-sitter-php.wasm',
};
return languageFileMap[language];

View file

@ -30,6 +30,7 @@
"tree-sitter-go": "^0.21.0",
"tree-sitter-java": "^0.21.0",
"tree-sitter-javascript": "^0.21.0",
"tree-sitter-php": "^0.23.12",
"tree-sitter-python": "^0.21.0",
"tree-sitter-rust": "^0.21.0",
"tree-sitter-typescript": "^0.21.0",
@ -4378,6 +4379,34 @@
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-php": {
"version": "0.23.12",
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz",
"integrity": "sha512-VwkBVOahhC2NYXK/Fuqq30NxuL/6c2hmbxEF4jrB7AyR5rLc7nT27mzF3qoi+pqx9Gy2AbXnGezF7h4MeM6YRA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.2.2",
"node-gyp-build": "^4.8.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-php/node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/tree-sitter-python": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.21.0.tgz",

View file

@ -62,6 +62,7 @@
"tree-sitter-go": "^0.21.0",
"tree-sitter-java": "^0.21.0",
"tree-sitter-javascript": "^0.21.0",
"tree-sitter-php": "^0.23.12",
"tree-sitter-python": "^0.21.0",
"tree-sitter-rust": "^0.21.0",
"tree-sitter-typescript": "^0.21.0",

View file

@ -8,7 +8,7 @@ export enum SupportedLanguages {
CSharp = 'csharp',
Go = 'go',
Rust = 'rust',
// PHP = 'php',
PHP = 'php',
// Ruby = 'ruby',
// Swift = 'swift',
}

View file

@ -102,6 +102,27 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
/^Run$/, // Run methods
/^Start$/, // Start methods
],
// PHP / Laravel
'php': [
/Controller$/, // UserController (class name convention)
/^handle$/, // Job::handle(), Listener::handle()
/^execute$/, // Command::execute()
/^boot$/, // ServiceProvider::boot()
/^register$/, // ServiceProvider::register()
/^__invoke$/, // Invokable controllers/actions
/^(index|show|store|update|destroy|create|edit)$/, // RESTful resource methods
/^(get|post|put|delete|patch)[A-Z]/, // Explicit HTTP method actions
/^run$/, // Command/Job run()
/^fire$/, // Event fire()
/^dispatch$/, // Dispatchable jobs
/Service$/, // UserService (Service layer)
/Repository$/, // UserRepository (Repository pattern)
/^find$/, // Repository::find()
/^findAll$/, // Repository::findAll()
/^save$/, // Repository::save()
/^delete$/, // Repository::delete()
],
};
// ============================================================================
@ -252,7 +273,12 @@ export function isTestFile(filePath: string): boolean {
p.includes('/tests/') ||
// C# test patterns
p.includes('.tests/') ||
p.includes('tests.cs')
p.includes('tests.cs') ||
// PHP/Laravel test patterns
p.endsWith('test.php') ||
p.endsWith('spec.php') ||
p.includes('/tests/feature/') ||
p.includes('/tests/unit/')
);
}

View file

@ -195,8 +195,70 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'c-cpp', entryPointMultiplier: 2.5, reason: 'c-app' };
}
// ========== PHP / LARAVEL FRAMEWORKS ==========
// Laravel routes (highest - these ARE the entry point definitions)
if (p.includes('/routes/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-routes' };
}
// Laravel controllers (very high - receive HTTP requests)
if ((p.includes('/http/controllers/') || p.includes('/controllers/')) && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-controller' };
}
// Laravel controller by file name convention
if (p.endsWith('controller.php')) {
return { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'laravel-controller-file' };
}
// Laravel console commands
if ((p.includes('/console/commands/') || p.includes('/commands/')) && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-command' };
}
// Laravel jobs (queue entry points)
if (p.includes('/jobs/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-job' };
}
// Laravel listeners (event-driven entry points)
if (p.includes('/listeners/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-listener' };
}
// Laravel middleware
if (p.includes('/http/middleware/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.5, reason: 'laravel-middleware' };
}
// Laravel service providers
if (p.includes('/providers/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.8, reason: 'laravel-provider' };
}
// Laravel policies
if (p.includes('/policies/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 2.0, reason: 'laravel-policy' };
}
// Laravel models (important but not entry points per se)
if (p.includes('/models/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-model' };
}
// Laravel services (Service Repository pattern)
if (p.includes('/services/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.8, reason: 'laravel-service' };
}
// Laravel repositories (Service Repository pattern)
if (p.includes('/repositories/') && p.endsWith('.php')) {
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' };
}
// ========== GENERIC PATTERNS ==========
// Any language: index files in API folders
if (p.includes('/api/') && (
p.endsWith('/index.ts') || p.endsWith('/index.js') ||
@ -235,7 +297,11 @@ export const FRAMEWORK_AST_PATTERNS = {
// Go patterns (function signatures)
'go-http': ['http.Handler', 'http.HandlerFunc', 'ServeHTTP'],
// PHP/Laravel
'laravel': ['Route::get', 'Route::post', 'Route::put', 'Route::delete',
'Route::resource', 'Route::apiResource', '#[Route('],
// Rust macros
'actix': ['#[get', '#[post', '#[put', '#[delete'],
'axum': ['Router::new'],

View file

@ -122,6 +122,37 @@ async function loadGoModulePath(repoRoot: string): Promise<GoModuleConfig | null
return null;
}
/** PHP Composer PSR-4 autoload config */
interface ComposerConfig {
/** Map of namespace prefix -> directory (e.g., "App\\" -> "app/") */
psr4: Map<string, string>;
}
async function loadComposerConfig(repoRoot: string): Promise<ComposerConfig | null> {
try {
const composerPath = path.join(repoRoot, 'composer.json');
const raw = await fs.readFile(composerPath, 'utf-8');
const composer = JSON.parse(raw);
const psr4Raw = composer.autoload?.['psr-4'] ?? {};
const psr4Dev = composer['autoload-dev']?.['psr-4'] ?? {};
const merged = { ...psr4Raw, ...psr4Dev };
const psr4 = new Map<string, string>();
for (const [ns, dir] of Object.entries(merged)) {
const nsNorm = (ns as string).replace(/\\+$/, '');
const dirNorm = (dir as string).replace(/\\/g, '/').replace(/\/+$/, '');
psr4.set(nsNorm, dirNorm);
}
if (isDev) {
console.log(`📦 Loaded ${psr4.size} PSR-4 mappings from composer.json`);
}
return { psr4 };
} catch {
return null;
}
}
// ============================================================================
// IMPORT PATH RESOLUTION
// ============================================================================
@ -143,6 +174,8 @@ const EXTENSIONS = [
'.go',
// Rust
'.rs', '/mod.rs',
// PHP
'.php', '.phtml',
];
/**
@ -581,6 +614,48 @@ function resolveGoPackage(
return matches;
}
// ============================================================================
// PHP PSR-4 IMPORT RESOLUTION
// ============================================================================
/**
* Resolve a PHP use-statement import path using PSR-4 mappings.
* e.g. "App\Http\Controllers\UserController" -> "app/Http/Controllers/UserController.php"
*/
function resolvePhpImport(
importPath: string,
composerConfig: ComposerConfig | null,
allFiles: Set<string>,
normalizedFileList: string[],
allFileList: string[],
index?: SuffixIndex,
): string | null {
// Normalize: replace backslashes with forward slashes
const normalized = importPath.replace(/\\/g, '/');
// Try PSR-4 resolution if composer.json was found
if (composerConfig) {
// Sort namespaces by length descending (longest match wins)
const sorted = [...composerConfig.psr4.entries()].sort((a, b) => b[0].length - a[0].length);
for (const [nsPrefix, dirPrefix] of sorted) {
const nsPrefixSlash = nsPrefix.replace(/\\/g, '/');
if (normalized.startsWith(nsPrefixSlash + '/') || normalized === nsPrefixSlash) {
const remainder = normalized.slice(nsPrefixSlash.length).replace(/^\//, '');
const filePath = dirPrefix + (remainder ? '/' + remainder : '') + '.php';
if (allFiles.has(filePath)) return filePath;
if (index) {
const result = index.getInsensitive(filePath);
if (result) return result;
}
}
}
}
// Fallback: suffix matching (works without composer.json)
const pathParts = normalized.split('/').filter(Boolean);
return suffixResolve(pathParts, normalizedFileList, allFileList, index);
}
// ============================================================================
// MAIN IMPORT PROCESSOR
// ============================================================================
@ -612,6 +687,7 @@ export const processImports = async (
const effectiveRoot = repoRoot || '';
const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
const goModule = await loadGoModulePath(effectiveRoot);
const composerConfig = await loadComposerConfig(effectiveRoot);
// Helper: add an IMPORTS edge + update import map
const addImportEdge = (filePath: string, resolvedPath: string) => {
@ -736,6 +812,15 @@ export const processImports = async (
// Fall through if no files found (package might be external)
}
// ---- PHP: handle namespace-based imports (use statements) ----
if (language === SupportedLanguages.PHP) {
const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
if (resolved) {
addImportEdge(file.path, resolved);
}
return;
}
// ---- Standard single-file resolution ----
const resolvedPath = resolveImportPath(
file.path,
@ -785,6 +870,7 @@ export const processImportsFromExtracted = async (
const effectiveRoot = repoRoot || '';
const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
const goModule = await loadGoModulePath(effectiveRoot);
const composerConfig = await loadComposerConfig(effectiveRoot);
const addImportEdge = (filePath: string, resolvedPath: string) => {
const sourceId = generateId('File', filePath);
@ -884,6 +970,16 @@ export const processImportsFromExtracted = async (
}
}
// PHP: handle namespace-based imports (use statements)
if (language === SupportedLanguages.PHP) {
const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
if (resolved) {
resolveCache.set(cacheKey, resolved);
addImportEdge(filePath, resolved);
}
continue;
}
// Standard resolution (has its own internal cache)
const resolvedPath = resolveImportPath(
filePath,

View file

@ -317,6 +317,85 @@ export const RUST_QUERIES = `
(impl_item trait: (generic_type type: (type_identifier) @heritage.trait) type: (type_identifier) @heritage.class) @heritage
`;
// PHP queries - works with tree-sitter-php (php_only grammar)
export const PHP_QUERIES = `
; ── Namespace ────────────────────────────────────────────────────────────────
(namespace_definition
name: (namespace_name) @name) @definition.namespace
; ── Classes ──────────────────────────────────────────────────────────────────
(class_declaration
name: (name) @name) @definition.class
; ── Interfaces ───────────────────────────────────────────────────────────────
(interface_declaration
name: (name) @name) @definition.interface
; ── Traits ───────────────────────────────────────────────────────────────────
(trait_declaration
name: (name) @name) @definition.trait
; ── Enums (PHP 8.1) ──────────────────────────────────────────────────────────
(enum_declaration
name: (name) @name) @definition.enum
; ── Top-level functions ───────────────────────────────────────────────────────
(function_definition
name: (name) @name) @definition.function
; ── Methods (including constructors) ─────────────────────────────────────────
(method_declaration
name: (name) @name) @definition.method
; ── Class properties (including Eloquent $fillable, $casts, etc.) ────────────
(property_declaration
(property_element
(variable_name
(name) @name))) @definition.property
; ── Imports: use statements ──────────────────────────────────────────────────
; Simple: use App\\Models\\User;
(namespace_use_declaration
(namespace_use_clause
(qualified_name) @import.source)) @import
; ── Function/method calls ────────────────────────────────────────────────────
; Regular function call: foo()
(function_call_expression
function: (name) @call.name) @call
; Method call: $obj->method()
(member_call_expression
name: (name) @call.name) @call
; Nullsafe method call: $obj?->method()
(nullsafe_member_call_expression
name: (name) @call.name) @call
; Static call: Foo::bar() (php_only uses scoped_call_expression)
(scoped_call_expression
name: (name) @call.name) @call
; ── Heritage: extends ────────────────────────────────────────────────────────
(class_declaration
name: (name) @heritage.class
(base_clause
[(name) (qualified_name)] @heritage.extends)) @heritage
; ── Heritage: implements ─────────────────────────────────────────────────────
(class_declaration
name: (name) @heritage.class
(class_interface_clause
[(name) (qualified_name)] @heritage.implements)) @heritage.impl
; ── Heritage: use trait (must capture enclosing class name) ──────────────────
(class_declaration
name: (name) @heritage.class
body: (declaration_list
(use_declaration
[(name) (qualified_name)] @heritage.trait))) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -327,5 +406,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
};

View file

@ -31,6 +31,12 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
if (filename.endsWith('.go')) return SupportedLanguages.Go;
// Rust
if (filename.endsWith('.rs')) return SupportedLanguages.Rust;
// PHP (all common extensions)
if (filename.endsWith('.php') || filename.endsWith('.phtml') ||
filename.endsWith('.php3') || filename.endsWith('.php4') ||
filename.endsWith('.php5') || filename.endsWith('.php8')) {
return SupportedLanguages.PHP;
}
return null;
};

View file

@ -9,6 +9,7 @@ import CPP from 'tree-sitter-cpp';
import CSharp from 'tree-sitter-c-sharp';
import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import PHP from 'tree-sitter-php';
import { SupportedLanguages } from '../../../config/supported-languages.js';
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
import { getLanguageFromFilename } from '../utils.js';
@ -28,6 +29,7 @@ interface ParsedNode {
endLine: number;
language: string;
isExported: boolean;
description?: string;
};
}
@ -100,6 +102,7 @@ const languageMap: Record<string, any> = {
[SupportedLanguages.CSharp]: CSharp,
[SupportedLanguages.Go]: Go,
[SupportedLanguages.Rust]: Rust,
[SupportedLanguages.PHP]: PHP.php_only,
};
const setLanguage = (language: SupportedLanguages, filePath: string): void => {
@ -185,6 +188,24 @@ const isNodeExported = (node: any, name: string, language: string): boolean => {
case 'cpp':
return false;
case 'php':
// Top-level classes/interfaces/traits are always accessible
// Methods/properties are exported only if they have 'public' modifier
while (current) {
if (current.type === 'class_declaration' ||
current.type === 'interface_declaration' ||
current.type === 'trait_declaration' ||
current.type === 'enum_declaration') {
return true;
}
if (current.type === 'visibility_modifier') {
return current.text === 'public';
}
current = current.parent;
}
// Top-level functions (no parent class) are globally accessible
return true;
default:
return false;
}
@ -200,6 +221,7 @@ const FUNCTION_NODE_TYPES = new Set([
'function_definition', 'async_function_declaration', 'async_arrow_function',
'method_declaration', 'constructor_declaration',
'local_function_statement', 'function_item', 'impl_item',
'anonymous_function_creation_expression', // PHP anonymous functions
]);
/** Walk up AST to find enclosing function, return its generateId or null for top-level */
@ -297,6 +319,23 @@ const BUILT_INS = new Set([
'mutex_lock', 'mutex_unlock', 'mutex_init',
'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
'get', 'put',
// PHP built-ins
'echo', 'isset', 'empty', 'unset', 'list', 'array', 'compact', 'extract',
'count', 'strlen', 'strpos', 'strrpos', 'substr', 'strtolower', 'strtoupper', 'trim',
'ltrim', 'rtrim', 'str_replace', 'str_contains', 'str_starts_with', 'str_ends_with',
'sprintf', 'vsprintf', 'printf', 'number_format',
'array_map', 'array_filter', 'array_reduce', 'array_push', 'array_pop', 'array_shift',
'array_unshift', 'array_slice', 'array_splice', 'array_merge', 'array_keys', 'array_values',
'array_key_exists', 'in_array', 'array_search', 'array_unique', 'usort', 'rsort',
'json_encode', 'json_decode', 'serialize', 'unserialize',
'intval', 'floatval', 'strval', 'boolval', 'is_null', 'is_string', 'is_int', 'is_array',
'is_object', 'is_numeric', 'is_bool', 'is_float',
'var_dump', 'print_r', 'var_export',
'date', 'time', 'strtotime', 'mktime', 'microtime',
'file_exists', 'file_get_contents', 'file_put_contents', 'is_file', 'is_dir',
'preg_match', 'preg_match_all', 'preg_replace', 'preg_split',
'header', 'session_start', 'session_destroy', 'ob_start', 'ob_end_clean', 'ob_get_clean',
'dd', 'dump',
]);
// ============================================================================
@ -409,6 +448,109 @@ const processBatch = (files: ParseWorkerInput[], onProgress?: (filesProcessed: n
return result;
};
// ============================================================================
// PHP Eloquent metadata extraction
// ============================================================================
/** Eloquent model properties whose array values are worth indexing */
const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
/** Eloquent relationship method names */
const ELOQUENT_RELATIONS = new Set([
'hasMany', 'hasOne', 'belongsTo', 'belongsToMany',
'morphTo', 'morphMany', 'morphOne', 'morphToMany', 'morphedByMany',
'hasManyThrough', 'hasOneThrough',
]);
function findDescendant(node: any, type: string): any {
if (node.type === type) return node;
for (const child of (node.children ?? [])) {
const found = findDescendant(child, type);
if (found) return found;
}
return null;
}
function extractStringContent(node: any): string | null {
if (!node) return null;
const content = node.children?.find((c: any) => c.type === 'string_content');
if (content) return content.text;
if (node.type === 'string_content') return node.text;
return null;
}
/**
* For a PHP property_declaration node, extract array values as a description string.
* Returns null if not an Eloquent model property or no array values found.
*/
function extractPhpPropertyDescription(propName: string, propDeclNode: any): string | null {
if (!ELOQUENT_ARRAY_PROPS.has(propName)) return null;
const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
if (!arrayNode) return null;
const items: string[] = [];
for (const child of (arrayNode.children ?? [])) {
if (child.type !== 'array_element_initializer') continue;
const children = child.children ?? [];
const arrowIdx = children.findIndex((c: any) => c.type === '=>');
if (arrowIdx !== -1) {
// key => value pair (used in $casts)
const key = extractStringContent(children[arrowIdx - 1]);
const val = extractStringContent(children[arrowIdx + 1]);
if (key && val) items.push(`${key}:${val}`);
} else {
// Simple value (used in $fillable, $hidden, etc.)
const val = extractStringContent(children[0]);
if (val) items.push(val);
}
}
return items.length > 0 ? items.join(', ') : null;
}
/**
* For a PHP method_declaration node, detect if it defines an Eloquent relationship.
* Returns description like "hasMany(Post)" or null.
*/
function extractEloquentRelationDescription(methodNode: any): string | null {
function findRelationCall(node: any): any {
if (node.type === 'member_call_expression') {
const children = node.children ?? [];
const objectNode = children.find((c: any) => c.type === 'variable_name' && c.text === '$this');
const nameNode = children.find((c: any) => c.type === 'name');
if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text)) return node;
}
for (const child of (node.children ?? [])) {
const found = findRelationCall(child);
if (found) return found;
}
return null;
}
const callNode = findRelationCall(methodNode);
if (!callNode) return null;
const relType = callNode.children?.find((c: any) => c.type === 'name')?.text;
const argsNode = callNode.children?.find((c: any) => c.type === 'arguments');
let targetModel: string | null = null;
if (argsNode) {
const firstArg = argsNode.children?.find((c: any) => c.type === 'argument');
if (firstArg) {
const classConstant = firstArg.children?.find((c: any) =>
c.type === 'class_constant_access_expression'
);
if (classConstant) {
targetModel = classConstant.children?.find((c: any) => c.type === 'name')?.text ?? null;
}
}
}
if (relType && targetModel) return `${relType}(${targetModel})`;
if (relType) return relType;
return null;
}
const processFileGroup = (
files: ParseWorkerInput[],
language: SupportedLanguages,
@ -515,6 +657,15 @@ const processFileGroup = (
const nodeName = nameNode.text;
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
let description: string | undefined;
if (language === SupportedLanguages.PHP) {
if (nodeLabel === 'Property' && captureMap['definition.property']) {
description = extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
} else if (nodeLabel === 'Method' && captureMap['definition.method']) {
description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
}
}
result.nodes.push({
id: nodeId,
label: nodeLabel,
@ -525,6 +676,7 @@ const processFileGroup = (
endLine: nameNode.endPosition.row,
language: language,
isExported: isNodeExported(nameNode, nodeName, language),
...(description !== undefined ? { description } : {}),
},
});

View file

@ -212,7 +212,7 @@ export const streamAllCSVsToDisk = async (
// Create writers for every node type up-front
const fileWriter = new BufferedCSVWriter(path.join(csvDir, 'file.csv'), 'id,name,filePath,content');
const folderWriter = new BufferedCSVWriter(path.join(csvDir, 'folder.csv'), 'id,name,filePath');
const codeElementHeader = 'id,name,filePath,startLine,endLine,isExported,content';
const codeElementHeader = 'id,name,filePath,startLine,endLine,isExported,content,description';
const functionWriter = new BufferedCSVWriter(path.join(csvDir, 'function.csv'), codeElementHeader);
const classWriter = new BufferedCSVWriter(path.join(csvDir, 'class.csv'), codeElementHeader);
const interfaceWriter = new BufferedCSVWriter(path.join(csvDir, 'interface.csv'), codeElementHeader);
@ -222,7 +222,7 @@ export const streamAllCSVsToDisk = async (
const processWriter = new BufferedCSVWriter(path.join(csvDir, 'process.csv'), 'id,label,heuristicLabel,processType,stepCount,communities,entryPointId,terminalId');
// Multi-language node types share the same CSV shape (no isExported column)
const multiLangHeader = 'id,name,filePath,startLine,endLine,content';
const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
const MULTI_LANG_TYPES = ['Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module'] as const;
const multiLangWriters = new Map<string, BufferedCSVWriter>();
@ -305,6 +305,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVNumber(node.properties.endLine, -1),
node.properties.isExported ? 'true' : 'false',
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
].join(','));
} else {
// Multi-language node types (Struct, Impl, Trait, Macro, etc.)
@ -318,6 +319,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVNumber(node.properties.startLine, -1),
escapeCSVNumber(node.properties.endLine, -1),
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
].join(','));
}
}

View file

@ -277,10 +277,10 @@ const getCopyQuery = (table: NodeTableName, filePath: string): string => {
}
// TypeScript/JS code element tables have isExported; multi-language tables do not
if (TABLES_WITH_EXPORTED.has(table)) {
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
}
// Multi-language tables (Struct, Impl, Trait, Macro, etc.)
return `COPY ${t}(id, name, filePath, startLine, endLine, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
};
/**
@ -317,10 +317,12 @@ export const insertNodeToKuzu = async (
} else if (label === 'Folder') {
query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
} else if (TABLES_WITH_EXPORTED.has(label)) {
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}})`;
const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}${descPart}})`;
} else {
// Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}})`;
const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}})`;
}
// Use per-query connection if dbPath provided (avoids lock conflicts)
@ -386,9 +388,11 @@ export const batchInsertNodesToKuzu = async (
} else if (label === 'Folder') {
query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
} else if (TABLES_WITH_EXPORTED.has(label)) {
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}`;
const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
} else {
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}`;
const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
}
await tempConn.query(query);

View file

@ -64,6 +64,7 @@ CREATE NODE TABLE Function (
endLine INT64,
isExported BOOLEAN,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -76,6 +77,7 @@ CREATE NODE TABLE Class (
endLine INT64,
isExported BOOLEAN,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -88,6 +90,7 @@ CREATE NODE TABLE Interface (
endLine INT64,
isExported BOOLEAN,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -100,6 +103,7 @@ CREATE NODE TABLE Method (
endLine INT64,
isExported BOOLEAN,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -112,6 +116,7 @@ CREATE NODE TABLE CodeElement (
endLine INT64,
isExported BOOLEAN,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -154,6 +159,7 @@ CREATE NODE TABLE Process (
// ============================================================================
// Generic code element with startLine/endLine for C, C++, Rust, Go, Java, C#
// description: optional metadata (e.g. Eloquent $fillable fields, relationship targets)
const CODE_ELEMENT_BASE = (name: string) => `
CREATE NODE TABLE \`${name}\` (
id STRING,
@ -162,6 +168,7 @@ CREATE NODE TABLE \`${name}\` (
startLine INT64,
endLine INT64,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
@ -266,6 +273,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
FROM Method TO \`Impl\`,
FROM Method TO Interface,
FROM Method TO \`Constructor\`,
FROM Method TO \`Property\`,
FROM \`Template\` TO \`Template\`,
FROM \`Template\` TO Function,
FROM \`Template\` TO Method,

View file

@ -8,6 +8,7 @@ import CPP from 'tree-sitter-cpp';
import CSharp from 'tree-sitter-c-sharp';
import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import PHP from 'tree-sitter-php';
import { SupportedLanguages } from '../../config/supported-languages.js';
let parser: Parser | null = null;
@ -23,6 +24,7 @@ const languageMap: Record<string, any> = {
[SupportedLanguages.CSharp]: CSharp,
[SupportedLanguages.Go]: Go,
[SupportedLanguages.Rust]: Rust,
[SupportedLanguages.PHP]: PHP.php_only,
};
export const loadParser = async (): Promise<Parser> => {