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>
488 lines
21 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|