Files
rspade_system/app/RSpade/CodeQuality/Rules/Common/RouteExists_CodeQualityRule.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

238 lines
8.8 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* RouteExistsRule - Validates that Rsx::Route() calls reference existing routes
*
* This rule checks both PHP and JavaScript files for Route() calls with literal
* string parameters and validates that the referenced controller and method
* combination actually exists as a route in the manifest.
*
* Example violations:
* - Rsx::Route('NonExistent_Controller')
* - Route('Some_Controller', 'missing_method')
*
* The rule only checks when both parameters are string literals, not variables.
*/
class RouteExists_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'ROUTE-EXISTS-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Route Target Exists Validation';
}
/**
* Get rule description
*/
public function get_description(): string
{
return 'Validates that Rsx::Route() calls reference controller methods that actually exist as routes';
}
/**
* Get file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.php', '*.js', '*.blade.php'];
}
/**
* Whether this rule is called during manifest scan
*
* IMPORTANT: This method should ALWAYS return false unless explicitly requested
* by the framework developer. Manifest-time checks are reserved for critical
* framework convention violations that need immediate developer attention.
*
* Rules executed during manifest scan will run on every file change in development,
* potentially impacting performance. Only enable this for rules that:
* - Enforce critical framework conventions that would break the application
* - Need to provide immediate feedback before code execution
* - Have been specifically requested to run at manifest-time by framework maintainers
*
* DEFAULT: Always return false unless you have explicit permission to do otherwise.
*/
public function is_called_during_manifest_scan(): bool
{
return false; // Only run during rsx:check, not during manifest build
}
/**
* Get default severity for this rule
*/
public function get_default_severity(): string
{
return 'high';
}
/**
* Check if a route exists using the same logic as Rsx::Route()
*/
private function route_exists(string $controller, string $method): bool
{
try {
// Use the same validation logic as Rsx::Route()
// If this doesn't throw an exception, the route exists
\App\RSpade\Core\Rsx::Route($controller, $method);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Check a file for violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if exception comment is present
if (strpos($contents, '@ROUTE-EXISTS-01-EXCEPTION') !== false) {
return;
}
// Pattern to match Rsx::Route and Rsx.Route calls (NOT plain Route())
// Matches both single and double parameter versions:
// - Rsx::Route('Controller') // PHP, defaults to 'index'
// - Rsx::Route('Controller', 'method') // PHP
// - Rsx.Route('Controller') // JavaScript, defaults to 'index'
// - Rsx.Route('Controller', 'method') // JavaScript
// Pattern for two parameters
$pattern_two_params = '/(?:Rsx::Route|Rsx\.Route)\s*\(\s*[\'"]([^\'"]+)[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/';
// Pattern for single parameter (defaults to 'index')
$pattern_one_param = '/(?:Rsx::Route|Rsx\.Route)\s*\(\s*[\'"]([^\'"]+)[\'"]\s*\)/';
// First check two-parameter calls
if (preg_match_all($pattern_two_params, $contents, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$full_match = $match[0];
$offset = $match[1];
$controller = $matches[1][$index][0];
$method = $matches[2][$index][0];
// Skip if contains template variables like {$variable}
if (str_contains($controller, '{$') || str_contains($controller, '${') ||
str_contains($method, '{$') || str_contains($method, '${')) {
continue;
}
// Skip if method starts with '#' - indicates unimplemented route
if (str_starts_with($method, '#')) {
continue;
}
// Skip if this route exists
if ($this->route_exists($controller, $method)) {
continue;
}
// Calculate line number
$line_number = substr_count(substr($contents, 0, $offset), "\n") + 1;
// Extract the line for snippet
$lines = explode("\n", $contents);
$code_snippet = isset($lines[$line_number - 1]) ? trim($lines[$line_number - 1]) : $full_match;
// Build suggestion
$suggestion = $this->build_suggestion($controller, $method);
$this->add_violation(
$file_path,
$line_number,
"Route target does not exist: {$controller}::{$method}",
$code_snippet,
$suggestion,
'high'
);
}
}
// Then check single-parameter calls (avoiding overlap with two-parameter calls)
if (preg_match_all($pattern_one_param, $contents, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$full_match = $match[0];
$offset = $match[1];
// Skip if this is actually a two-parameter call (has a comma after the first param)
$after_match_pos = $offset + strlen($full_match);
$chars_after = substr($contents, $after_match_pos, 10);
if (preg_match('/^\s*,/', $chars_after)) {
continue; // This is a two-parameter call, already handled above
}
$controller = $matches[1][$index][0];
$method = 'index'; // Default to 'index'
// Skip if contains template variables like {$variable}
if (str_contains($controller, '{$') || str_contains($controller, '${')) {
continue;
}
// Skip if this route exists
if ($this->route_exists($controller, $method)) {
continue;
}
// Calculate line number
$line_number = substr_count(substr($contents, 0, $offset), "\n") + 1;
// Extract the line for snippet
$lines = explode("\n", $contents);
$code_snippet = isset($lines[$line_number - 1]) ? trim($lines[$line_number - 1]) : $full_match;
// Build suggestion
$suggestion = $this->build_suggestion($controller, $method);
$this->add_violation(
$file_path,
$line_number,
"Route target does not exist: {$controller}::{$method}",
$code_snippet,
$suggestion,
'high'
);
}
}
}
/**
* Build suggestion for fixing the violation
*/
private function build_suggestion(string $controller, string $method): string
{
$suggestions = [];
// Simple suggestion since we're using the same validation as Rsx::Route()
$suggestions[] = "Route target does not exist: {$controller}::{$method}";
$suggestions[] = "\nTo fix this issue:";
$suggestions[] = "1. Correct the controller/method names if they're typos";
$suggestions[] = "2. Implement the missing route if it's a new feature:";
$suggestions[] = " - Create the controller if it doesn't exist";
$suggestions[] = " - Add the method with a #[Route] attribute";
$suggestions[] = "3. Use '#' prefix for unimplemented routes (recommended):";
$suggestions[] = " - Use Rsx::Route('Controller', '#index') for unimplemented index methods";
$suggestions[] = " - Use Rsx::Route('Controller', '#method_name') for other unimplemented methods";
$suggestions[] = " - Routes with '#' prefix will generate '#' URLs and bypass this validation";
$suggestions[] = " - Example: Rsx::Route('Backend_Users_Controller', '#index')";
return implode("\n", $suggestions);
}
}