Files
rspade_system/app/RSpade/Ide/Helper/Ide_Helper_Controller.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

488 lines
21 KiB
PHP

<?php
namespace App\RSpade\Ide\Helper;
use App\RSpade\Core\Manifest\Manifest;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
// Standalone controller for IDE integration - doesn't extend any base class
class Ide_Helper_Controller
{
/**
* Get git status for VS Code integration
*/
public function git_status(Request $request): JsonResponse
{
if (app()->environment('production')) {
return response()->json(['error' => 'Not available in production'], 403);
}
$base_path = base_path();
$output = [];
$return_code = 0;
\exec_safe("cd {$base_path} && git status --porcelain 2>&1", $output, $return_code);
if ($return_code !== 0) {
return response()->json(['error' => 'Git command failed', 'output' => implode("\n", $output)], 500);
}
$modified = [];
$added = [];
$conflicts = [];
foreach ($output as $line) {
if (strlen($line) < 4) continue;
$status = substr($line, 0, 2);
$file = trim(substr($line, 3));
// Parse git status codes
if (str_contains($status, 'U') || str_contains($status, 'A') && str_contains($status, 'A')) {
$conflicts[] = $file;
} elseif ($status[0] === 'A' || $status[1] === 'A' || $status === '??') {
$added[] = $file;
} elseif ($status[0] === 'M' || $status[1] === 'M' || $status[0] === 'R') {
$modified[] = $file;
}
}
return response()->json([
'modified' => $modified,
'added' => $added,
'conflicts' => $conflicts,
]);
}
/**
* Resolve various RSX identifiers to their file locations
* This endpoint is only available in development mode for IDE integration
*
* Priority order for resolution:
* 1. PHP class lookup (for Ajax_Endpoint routes and controllers)
* 2. RSX blade view lookup (for @rsx_extends, @rsx_include, rsx_view(), etc.)
* 3. Future: JavaScript class lookup
* 4. Future: Bundle/Module lookup
*/
public function resolve_class(Request $request): JsonResponse
{
// Double-check we're not in production
if (app()->environment('production')) {
return response()->json([
'error' => 'This endpoint is not available in production',
], 403);
}
$identifier = $request->query('class') ?? $request->query('identifier');
$method_name = $request->query('method');
$type = $request->query('type'); // Optional type hint: 'class', 'view', etc.
if (!$identifier) {
return response()->json([
'error' => 'Missing required parameter: class or identifier',
'found' => false,
], 400);
}
// Priority 1: Try as PHP class (if looks like a class name or type hint suggests it)
if (!$type || $type === 'class' || preg_match('/^[A-Z][A-Za-z0-9_]*$/', $identifier)) {
try {
$file_path = Manifest::php_find_class($identifier);
$metadata = Manifest::php_get_metadata_by_class($identifier);
// Try to find the line number where the class or method is defined
$line_number = 1;
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
if ($method_name) {
// Check if method metadata exists and has a different file (trait method)
$method_file_path = null;
if (isset($metadata['public_static_methods'][$method_name])) {
$method_metadata = $metadata['public_static_methods'][$method_name];
if (isset($method_metadata['file'])) {
// Method is from a trait - use trait file instead
$method_file_path = $method_metadata['file'];
$file_path = $method_file_path;
$absolute_path = base_path($file_path);
$content = file_exists($absolute_path) ? file_get_contents($absolute_path) : '';
$lines = explode("\n", $content);
}
// Use line number from metadata if available
if (isset($method_metadata['line'])) {
$line_number = $method_metadata['line'];
}
}
// If we don't have metadata or line number, search manually
if ($line_number === 1 && !empty($lines)) {
// Look for the specific method
$in_class_or_trait = false;
$brace_count = 0;
foreach ($lines as $index => $line) {
// Check if we're entering the class or trait
if (preg_match('/^\s*(class|trait)\s+\w+(\s|$)/', $line)) {
$in_class_or_trait = true;
}
// Track braces to know when we've left the class/trait
if ($in_class_or_trait) {
$brace_count += substr_count($line, '{');
$brace_count -= substr_count($line, '}');
// Look for the method definition (public/protected/private static function method_name)
if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
$line_number = $index + 1;
break;
}
// If we've left the class/trait, stop looking
if ($brace_count === 0 && strpos($line, '}') !== false) {
break;
}
}
}
}
} else {
// Find the line with the class definition
foreach ($lines as $index => $line) {
if (preg_match('/^\s*class\s+' . preg_quote($identifier, '/') . '(\s|$)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
}
$response_data = [
'found' => true,
'type' => 'class',
'file' => $file_path,
'line' => $line_number,
'metadata' => [
'namespace' => $metadata['namespace'] ?? null,
'extends' => $metadata['extends'] ?? null,
'fqcn' => $metadata['fqcn'] ?? null,
],
];
if ($method_name) {
$response_data['method'] = $method_name;
}
return response()->json($response_data);
} catch (RuntimeException $e) {
// Not a PHP class, continue to next priority
}
}
// Priority 2: Try as RSX blade view (if type hint suggests it or doesn't look like a class)
if (!$type || $type === 'view' || !preg_match('/Controller$/', $identifier)) {
try {
$file_path = Manifest::find_view($identifier);
// Find the line with @rsx_id definition
$line_number = 1;
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
// Look for @rsx_id('identifier') or @rsx_id("identifier")
foreach ($lines as $index => $line) {
if (preg_match('/@rsx_id\s*\(\s*[\'"]' . preg_quote($identifier, '/') . '[\'"]\s*\)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
return response()->json([
'found' => true,
'type' => 'view',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
} catch (RuntimeException $e) {
// Not a view, continue to next priority
}
}
// Priority 3: Try as bundle alias (if type hint suggests it or single lowercase word)
if (!$type || $type === 'bundle_alias' || preg_match('/^[a-z0-9]+$/', $identifier)) {
try {
// Load the rsx config to check bundle_aliases
$config_path = base_path('config/rsx.php');
if (file_exists($config_path)) {
$config = include $config_path;
if (isset($config['bundle_aliases'][$identifier])) {
$bundle_class = $config['bundle_aliases'][$identifier];
// Strip namespace and get just the class name
$class_parts = explode('\\', $bundle_class);
$class_name = end($class_parts);
// Now try to find this class in the manifest
try {
$file_path = Manifest::php_find_class($class_name);
// Find the line with the class definition
$line_number = 1;
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
foreach ($lines as $index => $line) {
if (preg_match('/^\s*class\s+' . preg_quote($class_name, '/') . '(\s|$)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
return response()->json([
'found' => true,
'type' => 'bundle_alias',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'resolved_class' => $bundle_class,
]);
} catch (RuntimeException $e) {
// Class not found in manifest
}
}
}
} catch (Exception $e) {
// Error reading config
}
}
// Priority 4: jqhtml template files (when type is 'jqhtml_template')
if ($type === 'jqhtml_template') {
try {
// Look for .jqhtml file with matching component name
$files = Manifest::get_all();
$component_snake = $this->camel_to_snake($identifier);
// Search for the .jqhtml file in manifest
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.jqhtml')) {
$basename = basename($file_path, '.jqhtml');
// Check if filename matches (either exact or snake_case version)
if ($basename === $component_snake ||
$this->snake_to_pascal($basename) === $identifier) {
// Find line with <Define:ComponentName> if present
$line_number = 1;
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
foreach ($lines as $index => $line) {
if (preg_match('/<Define:\s*' . preg_quote($identifier, '/') . '/', $line)) {
$line_number = $index + 1;
break;
}
}
}
return response()->json([
'found' => true,
'type' => 'jqhtml_template',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
}
}
}
} catch (Exception $e) {
// Not found as jqhtml template
}
}
// Priority 5: jqhtml JavaScript classes (when type is 'jqhtml_class')
if ($type === 'jqhtml_class') {
try {
// Look for JS class extending Component
$files = Manifest::get_all();
// Search for .js files containing the class
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.js')) {
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
// Check if this file contains a class with the right name extending Component
// Look for patterns like: class ComponentName extends Component
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Component/', $content)) {
$lines = explode("\n", $content);
$line_number = 1;
foreach ($lines as $index => $line) {
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
$line_number = $index + 1;
break;
}
}
return response()->json([
'found' => true,
'type' => 'jqhtml_class',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
}
}
}
}
} catch (Exception $e) {
// Not found as jqhtml class
}
}
// Priority 6: jqhtml class methods (when type is 'jqhtml_class_method')
if ($type === 'jqhtml_class_method' && $method_name) {
try {
// Special case: if method_name is 'data', look for 'on_load' instead
$search_method = ($method_name === 'data') ? 'on_load' : $method_name;
// Look for JS class extending Component and find the method
$files = Manifest::get_all();
// Search for .js files containing the class
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.js')) {
$absolute_path = base_path($file_path);
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
// Check if this file contains a class with the right name extending Component
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Component/', $content)) {
$lines = explode("\n", $content);
$line_number = 1;
$in_class = false;
$brace_count = 0;
foreach ($lines as $index => $line) {
// Check if we're entering the class
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
$in_class = true;
}
// Track braces to know when we've left the class
if ($in_class) {
$brace_count += substr_count($line, '{');
$brace_count -= substr_count($line, '}');
// Look for the method definition
// Match patterns like: methodName() { or async methodName() {
if (preg_match('/(?:async\s+)?' . preg_quote($search_method, '/') . '\s*\(/', $line)) {
$line_number = $index + 1;
break;
}
// If we've left the class, stop looking
if ($brace_count === 0 && strpos($line, '}') !== false) {
break;
}
}
}
return response()->json([
'found' => true,
'type' => 'jqhtml_class_method',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'method' => $method_name,
]);
}
}
}
}
} catch (Exception $e) {
// Not found as jqhtml class method
}
}
// Priority 7: Future - other types
// Nothing found
return response()->json([
'found' => false,
'error' => 'Identifier not found in manifest',
'identifier' => $identifier,
'searched_types' => ['class', 'view', 'bundle_alias', 'jqhtml_template', 'jqhtml_class', 'jqhtml_class_method'],
]);
}
/**
* Convert PascalCase or camelCase to snake_case
*/
private function camel_to_snake(string $input): string
{
// Insert underscore before uppercase letters
$result = preg_replace('/(?<!^)[A-Z]/', '_$0', $input);
return strtolower($result);
}
/**
* Convert snake_case to PascalCase
*/
private function snake_to_pascal(string $input): string
{
$parts = explode('_', $input);
return implode('_', array_map('ucfirst', $parts));
}
/**
* Get the inheritance lineage for a JavaScript class
* Returns array of parent class names from immediate parent to root
*/
public function js_lineage(Request $request): JsonResponse
{
// Double-check we're not in production
if (app()->environment('production')) {
return response()->json([
'error' => 'This endpoint is not available in production',
], 403);
}
$class_name = $request->query('class');
if (!$class_name) {
return response()->json([
'error' => 'Missing required parameter: class',
], 400);
}
$lineage = Manifest::js_get_lineage($class_name);
return response()->json([
'class' => $class_name,
'lineage' => $lineage,
]);
}
}