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>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

162
app/Console/Commands/CLAUDE.md Executable file
View File

@@ -0,0 +1,162 @@
# Console Commands Directory Documentation
This directory contains Artisan console commands used for maintenance, utilities, and administrative functions within the RSpade application.
## CLAUDE.md File Standards
Each CLAUDE.md file in the project follows these standards:
1. **Purpose**: The file begins with a brief synopsis of the directory's purpose and role in the application.
2. **File Index**: Contains a list of important files with their sizes and last modified dates.
- When visiting a directory, always run `ls -la` to get current file sizes
- If a file's current size differs from what's documented in CLAUDE.md, the file has changed
- Before trusting information about a changed file, first read the current version and update the CLAUDE.md documentation
3. **File Documentation**: For each important file, includes:
- Short description of purpose and functionality
- Key points of information necessary for understanding the file
- Important implementation details or architectural considerations
4. **Selective Coverage**: Only documents important files that contribute to understanding the codebase
- Images, temporary files, and generated content are typically not documented
- Configuration files, models, controllers, and other architectural components are always documented
5. **Additional Context**: May include other critical information about the directory's role in the system
Always check file sizes against the current state before relying on the documentation in CLAUDE.md files.
## Important Files
### Database Safety Guardrails
- **RestrictedDatabaseCommand.php** (2838 bytes) - Base class for restricted commands
- Provides standardized messaging for disabled database operations
- Enforces forward-only migration policy
- Offers guidance on alternative approaches
- Ensures safety for automated systems and AI agents
- **Database/WipeCommand.php** (946 bytes) - Override for db:wipe
- Prevents complete database destruction
- Protects against accidental data loss
- **Migrate/FreshCommand.php** (1528 bytes) - Override for migrate:fresh
- Prevents dropping and recreating all tables
- Maintains data integrity
- **Migrate/ResetCommand.php** (1109 bytes) - Override for migrate:reset
- Prevents rollback of all migrations
- Ensures forward-only migration strategy
- **Migrate/RefreshCommand.php** (1316 bytes) - Override for migrate:refresh
- Prevents reset and re-run of migrations
- Protects existing data
- **Migrate/RollbackCommand.php** (1343 bytes) - Override for migrate:rollback
- Prevents rollback of migrations
- Enforces schema evolution through new migrations only
### Schema Maintenance Commands
- **Maint_Migrate.php** (3482 bytes) - Enhanced migration command
- Extends Laravel's migrate command with additional maintenance steps
- Runs required table column checks
- Regenerates model constants
- Exports constants to JavaScript
- Usage: `php artisan maint:migrate`
- **Migrate/MigrateNormalizeSchema.php** - Normalizes database schema to framework standards
- Standardizes data types (INT→BIGINT, TEXT→LONGTEXT, FLOAT→DOUBLE)
- Converts character encoding to UTF8MB4 for full Unicode/emoji support
- Adds required columns (created_at, updated_at, created_by, updated_by)
- Adds indexes on timestamp columns
- Updates datetime precision to milliseconds
- Usage: `php artisan migrate:normalize_schema`
- **Note**: Automatically called during migrations - manual execution rarely needed
### Code Generation Commands
- **Migrate/MigrateRegenerateConstants.php** - Model constant generation
- Scans model files for static $enums properties
- Generates class constants for enum values
- Updates PHPDoc comments with property and method tags
- Adds $dates property with datetime columns
- Validates enum definitions for consistency
- Updates model files in-place with constants and docblocks
- Usage: `php artisan migrate:regenerate_constants`
- **Note**: Automatically called during migrations - manual execution rarely needed
### Content Management Commands
- **CreateSampleKnowledgeBase.php** (14626 bytes) - Sample content generation
- Creates sample knowledge base articles for testing
- Generates categories and content hierarchy
- Adds formatted content with Markdown
- Creates content relationships and metadata
- Usage: `php artisan create:sample-kb`
- **FeatureKnowledgeBaseArticles.php** (1594 bytes) - Content curation
- Selects and features knowledge base articles
- Updates featured status for homepage display
- Manages featured article ordering
- Usage: `php artisan feature:kb-articles`
### Utility Commands
- **GenerateSitemap.php** (4206 bytes) - SEO sitemap generation
- Creates XML sitemap for search engines
- Includes all public routes and content pages
- Sets priority and change frequency
- Optimizes for search engine indexing
- Usage: `php artisan generate:sitemap`
- **GetEnvironment.php** (579 bytes) - Environment diagnostic
- Displays current application environment
- Shows configuration values for debugging
- Helps with environment-specific troubleshooting
- Usage: `php artisan env:get`
## Command Categories
The commands are organized into several functional categories:
1. **Maintenance Commands** (prefix: `maint:`)
- Database and schema maintenance
- Model code generation
- System consistency checks
2. **Utility Commands** (prefix: `utility:`)
- Helper functions for development
- Code generation utilities
- Export functionality
3. **Content Commands** (no standard prefix)
- Sample content generation
- Content management and curation
- Seeding and fixture creation
4. **Diagnostic Commands** (usually prefixed with the subsystem name)
- System status and health checks
- Configuration validation
- Performance diagnostics
## Important Concepts
1. **Enum System Integration**: Several commands work with the model enum system, generating constants and JavaScript exports.
2. **Database Schema Management**: Commands ensure consistent schema structure and features across tables.
3. **Code Generation**: Commands generate code and documentation to reduce manual work and ensure consistency.
4. **Content Management**: Commands facilitate content seeding and management for testing and initial setup.
## Usage in Development Workflow
These commands are typically used in the following scenarios:
1. After database migrations to ensure schema consistency
2. During development to keep model constants in sync with enum definitions
3. For generating test data and sample content
4. As part of deployment processes to prepare the application
The commands can be run manually or as part of automated scripts, and many are integrated with Laravel's migration system to run automatically when migrations are executed.

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
/**
* Base class for framework developer commands
*
* Commands extending this class will be hidden from the artisan command list
* unless IS_FRAMEWORK_DEVELOPER=true is set in the .env file.
*
* These commands can still be called directly, but are hidden to avoid
* confusion for end users who shouldn't need to use them.
*/
abstract class FrameworkDeveloperCommand extends Command
{
/**
* Hide command from list unless framework developer mode is enabled
*
* @var bool
*/
protected $hidden = false;
/**
* Create a new command instance
*/
public function __construct()
{
parent::__construct();
// Hide command unless IS_FRAMEWORK_DEVELOPER flag is set
$this->hidden = !env('IS_FRAMEWORK_DEVELOPER', false);
}
/**
* Get the console command description with framework developer indicator
*
* @return string
*/
public function getDescription(): string
{
$description = parent::getDescription();
// Add prefix if in framework developer mode
if (env('IS_FRAMEWORK_DEVELOPER', false)) {
return "[Framework Dev] {$description}";
}
return $description;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GetEnvironment extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'env';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get the current application environment';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->line(app()->environment());
return 0;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
/**
* Override Laravel's key:generate to prevent accidental key regeneration
*
* This command replaces the default key:generate behavior with a safer version
* that requires explicit --force flag when a key already exists.
*/
class KeyGenerateOverride extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'key:generate
{--show : Display the key instead of modifying files}
{--force : Force the operation even if key exists}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Set the application key (requires --force if key exists)';
/**
* Execute the console command.
*/
public function handle()
{
$current_key = config('app.key');
// Check if key already exists and --force not provided
if ($current_key && !$this->option('force')) {
$this->error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->error(' APPLICATION KEY ALREADY EXISTS');
$this->error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->newLine();
$this->line('Current key: <fg=yellow>' . substr($current_key, 0, 20) . '...</>');
$this->newLine();
$this->warn('⚠️ Regenerating the application key will cause:');
$this->warn(' • All existing encrypted data becomes unreadable');
$this->warn(' • All user sessions invalidated (everyone logged out)');
$this->warn(' • All password reset tokens invalidated');
$this->warn(' • All remember-me cookies invalidated');
$this->newLine();
$this->error('If you really need to regenerate (THIS IS DESTRUCTIVE):');
$this->line(' <fg=red>php artisan key:generate --force</>');
$this->newLine();
$this->info('For first-time setup (when key is empty):');
$this->line(' <fg=green>php artisan key:generate</>');
$this->newLine();
$this->error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
return 1;
}
// If --force provided with existing key, show additional warning
if ($current_key && $this->option('force')) {
$this->warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->warn(' DESTRUCTIVE OPERATION - REGENERATING APPLICATION KEY');
$this->warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->newLine();
$this->error('This will DESTROY all encrypted data and invalidate all sessions!');
$this->newLine();
if (!$this->confirm('Are you absolutely sure you want to continue?', false)) {
$this->info('Operation cancelled.');
return 0;
}
$this->newLine();
}
// Generate new key
$key = $this->generate_random_key();
if ($this->option('show')) {
$this->line('<comment>' . $key . '</comment>');
return 0;
}
// Set key in .env file
if (!$this->set_key_in_environment_file($key)) {
$this->error('Failed to write key to .env file');
return 1;
}
$this->laravel['config']['app.key'] = $key;
$this->info('Application key set successfully.');
return 0;
}
/**
* Generate a random key for the application.
*/
protected function generate_random_key(): string
{
return 'base64:' . base64_encode(
Encrypter::generateKey($this->laravel['config']['app.cipher'])
);
}
/**
* Set the application key in the environment file.
*/
protected function set_key_in_environment_file(string $key): bool
{
$env_file = $this->laravel->environmentFilePath();
$current_key = $this->laravel['config']['app.key'];
if (strlen($current_key) !== 0) {
// Replace existing key
$replaced = preg_replace(
$this->key_replacement_pattern(),
'APP_KEY=' . $key,
$input = file_get_contents($env_file)
);
if ($replaced === $input || $replaced === null) {
$this->error('Unable to set application key. Manual setting required.');
return false;
}
} else {
// Add new key
$replaced = file_get_contents($env_file) . PHP_EOL . 'APP_KEY=' . $key . PHP_EOL;
}
file_put_contents($env_file, $replaced);
return true;
}
/**
* Get a regex pattern that will match APP_KEY with any random key.
*/
protected function key_replacement_pattern(): string
{
$escaped = preg_quote('=' . $this->laravel['config']['app.key'], '/');
return "/^APP_KEY{$escaped}/m";
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
class KeyGenerateSafe extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'key:generate:safe
{--show : Display the key instead of modifying files}
{--force : Force the operation to run when in production}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate application key only if one does not exist (prevents accidental regeneration)';
/**
* Execute the console command.
*/
public function handle()
{
$current_key = config('app.key');
// Check if key already exists
if ($current_key) {
$this->error('Application key already exists!');
$this->newLine();
$this->line('Current key: ' . $current_key);
$this->newLine();
$this->warn('Regenerating the application key will:');
$this->warn(' - Invalidate all existing sessions');
$this->warn(' - Break all encrypted data');
$this->warn(' - Require all users to log in again');
$this->newLine();
$this->error('If you really need to regenerate, use: php artisan key:generate --force');
$this->newLine();
return 1;
}
// Generate new key
$key = $this->generate_random_key();
if ($this->option('show')) {
$this->line('<comment>' . $key . '</comment>');
return 0;
}
// Check if .env file exists
$env_file = $this->laravel->environmentFilePath();
if (!file_exists($env_file)) {
$this->error('.env file not found at: ' . $env_file);
return 1;
}
// Set key in .env file
if (!$this->set_key_in_environment_file($key)) {
$this->error('Failed to write key to .env file');
return 1;
}
$this->laravel['config']['app.key'] = $key;
$this->info('Application key set successfully.');
return 0;
}
/**
* Generate a random key for the application.
*/
protected function generate_random_key(): string
{
return 'base64:' . base64_encode(
Encrypter::generateKey($this->laravel['config']['app.cipher'])
);
}
/**
* Set the application key in the environment file.
*/
protected function set_key_in_environment_file(string $key): bool
{
$env_file = $this->laravel->environmentFilePath();
$current_key = $this->laravel['config']['app.key'];
if (strlen($current_key) !== 0) {
// Replace existing key
$replaced = preg_replace(
$this->key_replacement_pattern(),
'APP_KEY=' . $key,
$input = file_get_contents($env_file)
);
if ($replaced === $input || $replaced === null) {
$this->error('Unable to set application key. Manual setting required.');
return false;
}
} else {
// Add new key
$replaced = file_get_contents($env_file) . PHP_EOL . 'APP_KEY=' . $key . PHP_EOL;
}
file_put_contents($env_file, $replaced);
return true;
}
/**
* Get a regex pattern that will match APP_KEY with any random key.
*/
protected function key_replacement_pattern(): string
{
$escaped = preg_quote('=' . $this->laravel['config']['app.key'], '/');
return "/^APP_KEY{$escaped}/m";
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Log;
/**
* Session Cleanup Command
* ========================
*
* PURPOSE:
* This command performs routine cleanup of expired and abandoned sessions from
* the database to prevent unbounded growth of the sessions table.
*
* CLEANUP RULES:
* 1. Guest sessions (user_id = null, site_id = null) older than 24 hours
* 2. Any sessions older than 3 years (regardless of user/site association)
*
* IMPLEMENTATION:
* - Uses raw DB::statement() for efficiency
* - Runs hourly via Laravel scheduler
* - Tracks deletion counts for monitoring
*
* WHY RAW SQL:
* - More efficient for bulk deletions
* - Clear audit trail in logs
* - Avoids ORM overhead for maintenance tasks
*
* INDEXING:
* The sessions table has indexes on:
* - user_id
* - site_id
* - updated_at
* - (updated_at, user_id) composite
* These indexes ensure efficient query execution.
*/
class SessionCleanupCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'session:cleanup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean up expired and abandoned sessions from the database';
/**
* Execute the console command.
*/
public function handle()
{
$start_time = microtime(true);
// Delete guest sessions older than 24 hours
// Using MySQL date functions for accuracy and performance
$guest_count = DB::delete('
DELETE FROM sessions
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)
AND user_id IS NULL
AND site_id IS NULL
');
// Delete any sessions older than 3 years
$old_count = DB::delete('
DELETE FROM sessions
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 1 YEAR)
');
$total_deleted = $guest_count + $old_count;
$execution_time = round(microtime(true) - $start_time, 3);
// Log results
if ($total_deleted > 0) {
$this->info('Session cleanup completed:');
$this->info(" • Guest sessions (>24h): {$guest_count} deleted");
$this->info(" • Old sessions (>3y): {$old_count} deleted");
$this->info(" • Total: {$total_deleted} sessions removed");
$this->info(" • Execution time: {$execution_time}s");
// Also log to Laravel log for monitoring
Log::info("Session cleanup: {$total_deleted} sessions deleted", [
'guest_sessions' => $guest_count,
'old_sessions' => $old_count,
'execution_time' => $execution_time,
]);
} else {
$this->info("No sessions to clean up (execution time: {$execution_time}s)");
}
return 0;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Console\Commands\Temp;
use Illuminate\Console\Command;
use App\RSpade\Core\Build_Manager;
class ClearCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'temp:clear
{--older-than=0 : Clear files older than N hours}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear temporary files';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$older_than = (int) $this->option('older-than');
if ($older_than > 0) {
$count = Build_Manager::clear_temp($older_than);
$this->info("Cleared $count temporary files older than $older_than hours.");
} else {
$count = Build_Manager::clear_temp();
$this->info("Cleared all $count temporary files.");
}
return 0;
}
}

75
app/Console/Kernel.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// Run task scheduler every minute
$schedule->command('task:scheduler')
->everyMinute()
->runInBackground()
->withoutOverlapping();
// Generate sitemap every 3 hours
$schedule->command('sitemap:generate --queue')
->cron('0 */3 * * *')
->runInBackground()
->withoutOverlapping();
// Run task cleanup daily
$schedule->command('task:scheduler --cleanup')
->daily()
->at('02:00')
->runInBackground()
->withoutOverlapping();
// Run session cleanup every hour
$schedule->command('session:cleanup')
->hourly()
->runInBackground()
->withoutOverlapping();
// Process geocoding tasks hourly
$schedule->command('geocoding:process --limit=20')
->hourly()
->runInBackground()
->withoutOverlapping();
// Process email queue every minute
$schedule->command('email:process --batch=10')
->everyMinute()
->runInBackground()
->withoutOverlapping();
// Process document conversions every 5 minutes
$schedule->command('documents:process --limit=10')
->everyFiveMinutes()
->runInBackground()
->withoutOverlapping();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands'); // Application commands
$this->load(__DIR__.'/../RSpade/Commands'); // Framework commands
require base_path('routes/console.php');
}
}

118
app/Constants.php Executable file
View File

@@ -0,0 +1,118 @@
<?php
namespace App;
/**
* Application-wide constants
*
* This file contains static resources and constants that are used throughout the application.
*
* Purpose:
* - Centralizes commonly used resources like state lists, MIME types, etc.
* - Provides a single source of truth for these constants
* - Makes maintenance easier - edit once, use everywhere
* - Improves consistency across the application
*
* Usage:
* - Include this file where constants are needed using: use App\Constants;
* - Access constants via the class constants (e.g., Constants::STATES)
*
* Guidelines:
* - Keep this file as a catchall for non-application-specific constants
* - Group related constants in meaningful arrays/classes
* - Add clear comments for each constant group
* - Use UPPERCASE for constant names
* - For long lists, maintain alphabetical order where appropriate
*
* Examples of appropriate constants:
* - US States and territories
* - Country codes
* - MIME types
* - Common date/time formats
* - Standard measurement units
* - Common regular expressions (email, phone, etc.)
*/
class Constants
{
// User ID for CLI operations
const CLI_USER = 'cli';
// System user email
const SYSTEM_USER_EMAIL = 'system@rspade.local';
// System user access level
const SYSTEM_USER_ACCESS_LEVEL = 90;
/**
* US States and territories with abbreviations
* Array of state abbreviations to full state names, in alphabetical order
*/
public const STATES = [
'AL' => 'Alabama',
'AK' => 'Alaska',
'AZ' => 'Arizona',
'AR' => 'Arkansas',
'CA' => 'California',
'CO' => 'Colorado',
'CT' => 'Connecticut',
'DE' => 'Delaware',
'DC' => 'District of Columbia',
'FL' => 'Florida',
'GA' => 'Georgia',
'HI' => 'Hawaii',
'ID' => 'Idaho',
'IL' => 'Illinois',
'IN' => 'Indiana',
'IA' => 'Iowa',
'KS' => 'Kansas',
'KY' => 'Kentucky',
'LA' => 'Louisiana',
'ME' => 'Maine',
'MD' => 'Maryland',
'MA' => 'Massachusetts',
'MI' => 'Michigan',
'MN' => 'Minnesota',
'MS' => 'Mississippi',
'MO' => 'Missouri',
'MT' => 'Montana',
'NE' => 'Nebraska',
'NV' => 'Nevada',
'NH' => 'New Hampshire',
'NJ' => 'New Jersey',
'NM' => 'New Mexico',
'NY' => 'New York',
'NC' => 'North Carolina',
'ND' => 'North Dakota',
'OH' => 'Ohio',
'OK' => 'Oklahoma',
'OR' => 'Oregon',
'PA' => 'Pennsylvania',
'RI' => 'Rhode Island',
'SC' => 'South Carolina',
'SD' => 'South Dakota',
'TN' => 'Tennessee',
'TX' => 'Texas',
'UT' => 'Utah',
'VT' => 'Vermont',
'VA' => 'Virginia',
'WA' => 'Washington',
'WV' => 'West Virginia',
'WI' => 'Wisconsin',
'WY' => 'Wyoming'
];
/**
* Common date formats
*/
public const DATE_FORMATS = [
'SHORT' => 'm/d/Y',
'MEDIUM' => 'M j, Y',
'LONG' => 'F j, Y',
'TIME' => 'g:i A',
'DATETIME' => 'M j, Y g:i A'
];
// Additional constants can be added here as needed:
// const MIME_TYPES = [...];
// const COUNTRY_CODES = [...];
// etc.
}

View File

@@ -0,0 +1,259 @@
<?php
namespace App\Database;
use Illuminate\Database\Eloquent\Builder;
/**
* Custom Eloquent query builder that prevents eager loading and unsafe operations
*
* This builder overrides dangerous methods to enforce RSpade framework safety rules:
* - All eager loading methods throw exceptions (with/withCount/etc)
* - DELETE without WHERE clause throws exception to prevent accidental mass deletion
*/
class RestrictedEloquentBuilder extends Builder
{
/**
* Prevent eager loading via with()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function with($relations, $callback = null)
{
// Allow empty with() calls (Laravel uses these internally)
if (empty($relations)) {
return parent::with($relations, $callback);
}
// Also allow if relations is an empty array
if (is_array($relations) && count($relations) === 0) {
return parent::with($relations, $callback);
}
// Throw exception for actual eager loading attempts
throw new \RuntimeException(
'Eager loading via with() is not allowed in the RSpade framework. ' .
'Use explicit queries for each relationship instead. ' .
'Attempted to eager load: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via withCount()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function withCount($relations)
{
// Allow empty withCount() calls
if (empty($relations)) {
return parent::withCount($relations);
}
throw new \RuntimeException(
'Eager loading counts via withCount() is not allowed in the RSpade framework. ' .
'Use explicit count queries instead. ' .
'Attempted to eager load counts for: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via withMax()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withMax($relation, $column)
{
// Allow empty withMax() calls
if (empty($relation)) {
return parent::withMax($relation, $column);
}
throw new \RuntimeException(
'Eager loading max via withMax() is not allowed in the RSpade framework. ' .
'Use explicit max queries instead.'
);
}
/**
* Prevent eager loading via withMin()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withMin($relation, $column)
{
// Allow empty withMin() calls
if (empty($relation)) {
return parent::withMin($relation, $column);
}
throw new \RuntimeException(
'Eager loading min via withMin() is not allowed in the RSpade framework. ' .
'Use explicit min queries instead.'
);
}
/**
* Prevent eager loading via withSum()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withSum($relation, $column)
{
// Allow empty withSum() calls
if (empty($relation)) {
return parent::withSum($relation, $column);
}
throw new \RuntimeException(
'Eager loading sum via withSum() is not allowed in the RSpade framework. ' .
'Use explicit sum queries instead.'
);
}
/**
* Prevent eager loading via withAvg()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withAvg($relation, $column)
{
// Allow empty withAvg() calls
if (empty($relation)) {
return parent::withAvg($relation, $column);
}
throw new \RuntimeException(
'Eager loading avg via withAvg() is not allowed in the RSpade framework. ' .
'Use explicit avg queries instead.'
);
}
/**
* Prevent eager loading via withExists()
*
* @param array|string $relation
* @return $this
* @throws \RuntimeException
*/
public function withExists($relation)
{
// Allow empty withExists() calls
if (empty($relation)) {
return parent::withExists($relation);
}
throw new \RuntimeException(
'Eager loading exists via withExists() is not allowed in the RSpade framework. ' .
'Use explicit exists queries instead.'
);
}
/**
* Prevent eager loading via withAggregate()
*
* @param mixed $relations
* @param string $column
* @param string $function
* @return $this
* @throws \RuntimeException
*/
public function withAggregate($relations, $column, $function = null)
{
// Allow empty withAggregate() calls
if (empty($relations)) {
return parent::withAggregate($relations, $column, $function);
}
throw new \RuntimeException(
'Eager loading aggregates via withAggregate() is not allowed in the RSpade framework. ' .
'Use explicit aggregate queries instead.'
);
}
/**
* Prevent lazy eager loading via has()
* Note: has() is different - it's for filtering, not loading
* But we'll still prevent it if it tries to eager load
*
* @param string $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?\Closure $callback = null)
{
// has() is allowed as it's for filtering, not eager loading
// But log it for monitoring
if (app()->environment() !== 'testing') {
logger()->debug('has() query used on relation', [
'model' => get_class($this->model),
'relation' => $relation,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5)
]);
}
return parent::has($relation, $operator, $count, $boolean, $callback);
}
/**
* Override delete to prevent deleting all records without WHERE clause
*
* @param mixed $id
* @return int
* @throws \RuntimeException
*/
public function delete($id = null)
{
// If $id is provided, allow it (deleting by primary key)
if ($id !== null) {
return parent::delete($id);
}
// Check if there are any WHERE clauses on the underlying query
$baseQuery = $this->getQuery();
if (empty($baseQuery->wheres)) {
// @PHP-DB-01-EXCEPTION - DB::table() mentioned in error message, not used
shouldnt_happen(
'Attempted to delete all records from ' . $this->getModel()->getTable() . ' without WHERE clause. ' .
'This operation is forbidden to prevent accidental data loss. ' .
'If you truly need to delete all records, use DB::table()->truncate() or add a WHERE clause.'
);
}
return parent::delete();
}
/**
* Format relations for error messages
*
* @param mixed $relations
* @return string
*/
protected function format_relations($relations)
{
if (is_array($relations)) {
return implode(', ', array_keys($relations));
}
return (string) $relations;
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace App\Database;
use Illuminate\Database\Eloquent\Collection;
/**
* Custom Eloquent collection that prevents eager loading
*
* This collection overrides all eager loading methods to throw exceptions
* when any form of relationship preloading is attempted on collections.
*/
class RestrictedEloquentCollection extends Collection
{
/**
* Prevent eager loading via load()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function load($relations)
{
throw new \RuntimeException(
'Eager loading on collections via load() is not allowed in the RSpade framework. ' .
'Use explicit queries for each relationship instead. ' .
'Attempted to eager load: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via loadMissing()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function loadMissing($relations)
{
throw new \RuntimeException(
'Conditional eager loading on collections via loadMissing() is not allowed in the RSpade framework. ' .
'Use explicit queries for each relationship instead. ' .
'Attempted to conditionally eager load: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via loadCount()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function loadCount($relations)
{
throw new \RuntimeException(
'Eager loading counts on collections via loadCount() is not allowed in the RSpade framework. ' .
'Use explicit count queries instead. ' .
'Attempted to eager load counts for: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via loadMax()
*
* @param array|string $relations
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function loadMax($relations, $column)
{
throw new \RuntimeException(
'Max eager loading on collections via loadMax() is not allowed in the RSpade framework. ' .
'Use explicit max queries instead.'
);
}
/**
* Prevent eager loading via loadMin()
*
* @param array|string $relations
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function loadMin($relations, $column)
{
throw new \RuntimeException(
'Min eager loading on collections via loadMin() is not allowed in the RSpade framework. ' .
'Use explicit min queries instead.'
);
}
/**
* Prevent eager loading via loadSum()
*
* @param array|string $relations
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function loadSum($relations, $column)
{
throw new \RuntimeException(
'Sum eager loading on collections via loadSum() is not allowed in the RSpade framework. ' .
'Use explicit sum queries instead.'
);
}
/**
* Prevent eager loading via loadAvg()
*
* @param array|string $relations
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function loadAvg($relations, $column)
{
throw new \RuntimeException(
'Avg eager loading on collections via loadAvg() is not allowed in the RSpade framework. ' .
'Use explicit avg queries instead.'
);
}
/**
* Prevent eager loading via loadExists()
*
* @param array|string $relations
* @return $this
* @throws \RuntimeException
*/
public function loadExists($relations)
{
throw new \RuntimeException(
'Exists eager loading on collections via loadExists() is not allowed in the RSpade framework. ' .
'Use explicit exists queries instead.'
);
}
/**
* Prevent eager loading via loadAggregate()
*
* @param mixed $relations
* @param string $column
* @param string $function
* @return $this
* @throws \RuntimeException
*/
public function loadAggregate($relations, $column, $function = null)
{
throw new \RuntimeException(
'Aggregate eager loading on collections via loadAggregate() is not allowed in the RSpade framework. ' .
'Use explicit aggregate queries instead.'
);
}
/**
* Prevent eager loading via loadMorph()
*
* @param string $relation
* @param array $relations
* @return $this
* @throws \RuntimeException
*/
public function loadMorph($relation, $relations)
{
throw new \RuntimeException(
'Morphed eager loading on collections via loadMorph() is not allowed in the RSpade framework. ' .
'Use explicit queries for each relationship instead.'
);
}
/**
* Prevent eager loading via loadMorphCount()
*
* @param string $relation
* @param array $relations
* @return $this
* @throws \RuntimeException
*/
public function loadMorphCount($relation, $relations)
{
throw new \RuntimeException(
'Morphed count eager loading on collections via loadMorphCount() is not allowed in the RSpade framework. ' .
'Use explicit count queries instead.'
);
}
/**
* Format relations for error messages
*
* @param mixed $relations
* @return string
*/
protected function format_relations($relations)
{
if (is_array($relations)) {
$formatted = [];
foreach ($relations as $key => $value) {
if (is_string($key)) {
$formatted[] = $key;
} else {
$formatted[] = $value;
}
}
return implode(', ', $formatted);
}
return (string) $relations;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Exceptions;
use Exception;
class MassAssignmentException extends Exception
{
protected $model_class;
protected $attempted_fields;
protected $method_name;
/**
* Create a new mass assignment exception
*
* @param string $model_class The model class that was targeted
* @param array $attempted_fields The fields that were attempted to be mass assigned
* @param bool $was_force Whether forceFill was attempted
* @param bool $is_static Whether a static method was called
* @param string|null $method_name Specific method name if relevant
*/
public function __construct($model_class, array $attempted_fields = [], $was_force = false, $is_static = false, $method_name = null)
{
$this->model_class = $model_class;
$this->attempted_fields = $attempted_fields;
$this->method_name = $method_name ?: ($was_force ? 'forceFill' : 'fill');
$message = $this->build_message($is_static);
parent::__construct($message);
}
/**
* Build the exception message with helpful guidance
*
* @param bool $is_static
* @return string
*/
protected function build_message($is_static)
{
$short_class = class_basename($this->model_class);
$var_name = '$' . strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $short_class));
$fields_list = implode(', ', $this->attempted_fields);
$message = "🚫 MASS ASSIGNMENT PROHIBITED IN RSPADE FRAMEWORK\n\n";
$message .= "Model: {$this->model_class}\n";
$message .= "Method: {$this->method_name}()\n";
if (!empty($this->attempted_fields)) {
$message .= "Attempted fields: {$fields_list}\n";
}
$message .= "\n";
$message .= "The RSpade framework prohibits mass assignment for security and code clarity.\n";
$message .= "All model fields must be explicitly assigned one by one.\n\n";
$message .= "❌ INCORRECT (mass assignment):\n";
if ($is_static) {
if ($this->method_name === 'create') {
$message .= "{$short_class}::create(\$request->all());\n";
$message .= "{$short_class}::create(\$request->validated());\n";
} elseif ($this->method_name === 'firstOrCreate') {
$message .= "{$short_class}::firstOrCreate(['email' => \$email], \$data);\n";
} elseif ($this->method_name === 'updateOrCreate') {
$message .= "{$short_class}::updateOrCreate(['id' => \$id], \$data);\n";
}
} else {
if ($this->method_name === 'update') {
$message .= "{$var_name}->update(\$request->all());\n";
$message .= "{$var_name}->update(\$request->validated());\n";
} else {
$message .= "{$var_name}->fill(\$request->all());\n";
$message .= "{$var_name}->forceFill(\$data);\n";
}
}
$message .= "\n✅ CORRECT (explicit field assignment):\n";
// Generate example based on attempted fields
if (!empty($this->attempted_fields)) {
if ($is_static || $this->method_name === 'update') {
if ($this->method_name === 'update') {
$message .= "{$var_name} = {$short_class}::find(\$id);\n";
} else {
$message .= "{$var_name} = new {$short_class}();\n";
}
foreach ($this->attempted_fields as $field) {
$message .= "{$var_name}->{$field} = \$request->input('{$field}');\n";
}
$message .= "{$var_name}->save();\n";
}
} else {
// Generic example
$message .= "{$var_name} = new {$short_class}();\n";
$message .= "{$var_name}->name = \$request->input('name');\n";
$message .= "{$var_name}->email = \$request->input('email');\n";
$message .= "{$var_name}->status = \$request->input('status');\n";
$message .= "{$var_name}->save();\n";
}
$message .= "\n";
$message .= "For finding and updating:\n";
$message .= "{$var_name} = {$short_class}::where('email', \$email)->first();\n";
$message .= "if (!{$var_name}) {\n";
$message .= " {$var_name} = new {$short_class}();\n";
$message .= " {$var_name}->email = \$email;\n";
$message .= "}\n";
$message .= "{$var_name}->name = \$request->input('name');\n";
$message .= "{$var_name}->save();\n";
$message .= "\n";
$message .= "Benefits of explicit assignment:\n";
$message .= "• Clear visibility of what fields are being set\n";
$message .= "• Protection against unexpected field injection\n";
$message .= "• Easier to debug and maintain\n";
$message .= "• Type safety with IDE autocompletion\n";
$message .= "• No need for \$fillable or \$guarded arrays\n";
$message .= "\n";
$message .= "Note: The \$fillable property is not needed in RSpade models.\n";
$message .= "Remove any \$fillable or \$guarded properties from your models.\n";
return $message;
}
/**
* Get the model class that triggered the exception
*
* @return string
*/
public function get_model_class()
{
return $this->model_class;
}
/**
* Get the fields that were attempted to be mass assigned
*
* @return array
*/
public function get_attempted_fields()
{
return $this->attempted_fields;
}
}

View File

77
app/Http/Kernel.php Executable file
View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
// Custom RSX middleware
\App\Http\Middleware\Gatekeeper::class, // Must run early to protect all routes
\App\Http\Middleware\CheckMigrationMode::class,
\App\Http\Middleware\PlaywrightTestMode::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// Session middleware removed for custom session handler
// \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Jqhtml\LaravelBridge\Middleware\JqhtmlErrorMiddleware::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
// 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, // Removed for custom session handler
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
// DISABLED 2025-09-14: RsxMiddleware removed as RSX routing now handled via 404 exception handler
// To re-enable: uncomment the line below and rename RsxMiddleware.php.disabled back to .php
// 'rsx' => \App\Http\Middleware\RsxMiddleware::class,
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
class CheckMigrationMode
{
protected $flag_file = '/var/www/html/.migrating';
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Only check migration mode in development environment
if (!app()->environment('production') && file_exists($this->flag_file)) {
$session_info = json_decode(file_get_contents($this->flag_file), true);
$started_at = $session_info['started_at'] ?? 'unknown';
// Create a detailed error message
$message = "🚧 Database Migration in Progress\n\n";
$message .= "A database migration session is currently active.\n";
$message .= "Started at: {$started_at}\n\n";
$message .= "The application is temporarily unavailable to ensure data integrity.\n\n";
$message .= "To complete the migration session, run one of these commands:\n";
$message .= " • php artisan migrate:commit - Keep the changes\n";
$message .= " • php artisan migrate:rollback - Revert to snapshot\n\n";
$message .= "For status: php artisan migrate:status";
// Throw service unavailable exception
throw new ServiceUnavailableHttpException(
null,
$message
);
}
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,176 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Gatekeeper
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Check if gatekeeper is enabled
if (!config('rsx.gatekeeper.enabled')) {
return $next($request);
}
// Always allow CLI requests
if (php_sapi_name() === 'cli') {
return $next($request);
}
// Always allow IDE helper endpoints for VS Code extension integration
if (str_starts_with($request->path(), '_idehelper')) {
return $next($request);
}
// Check if request is whitelisted (localhost without reverse proxy headers)
if ($this->is_whitelisted($request)) {
return $next($request);
}
// Check if user has valid authentication cookie
$cookie_name = config('rsx.gatekeeper.cookie_name', 'gatekeeper_auth');
$password = config('rsx.gatekeeper.password');
if (!$password) {
throw new \Exception('Gatekeeper enabled but no password configured. Set GATEKEEPER_PASSWORD in .env');
}
$cookie_value = $request->cookie($cookie_name);
$expected_hash = hash('sha256', $password);
// If authenticated, renew cookie and continue
if ($cookie_value === $expected_hash) {
$lifetime_hours = config('rsx.gatekeeper.cookie_lifetime_hours', 12);
$cookie = cookie($cookie_name, $expected_hash, 60 * $lifetime_hours);
$response = $next($request);
// Only add cookie to regular responses, not binary file responses
if (method_exists($response, 'withCookie')) {
return $response->withCookie($cookie);
}
return $response;
}
// Handle login POST request
if ($request->isMethod('POST') && $request->path() === '_gatekeeper/login') {
return $this->handle_login($request);
}
// Show login page
return $this->show_login_page($request);
}
/**
* Check if the request is whitelisted (localhost without reverse proxy headers)
*/
private function is_whitelisted(Request $request): bool
{
// Get the client IP
$ip = $request->ip();
// List of localhost IPs
$localhost_ips = [
'127.0.0.1',
'localhost',
'::1',
'0.0.0.0',
];
// Check if IP matches localhost patterns
$is_localhost = in_array($ip, $localhost_ips) ||
str_starts_with($ip, '127.') ||
$ip === '::1';
if (!$is_localhost) {
return false;
}
// Check for reverse proxy headers - if present, this is NOT a true localhost request
$proxy_headers = [
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED_HOST',
'HTTP_X_FORWARDED_PORT',
'HTTP_X_FORWARDED_PROTO',
'HTTP_X_FORWARDED_SERVER',
'HTTP_X_REAL_IP',
'HTTP_X_ORIGINAL_URL',
'HTTP_FORWARDED',
'HTTP_CLIENT_IP',
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_TRUE_CLIENT_IP', // Cloudflare Enterprise
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_VIA',
];
foreach ($proxy_headers as $header) {
if (!empty($_SERVER[$header])) {
// Reverse proxy header detected - force authentication
return false;
}
}
// Also check Laravel's request headers
if ($request->headers->has('X-Forwarded-For') ||
$request->headers->has('X-Forwarded-Host') ||
$request->headers->has('X-Forwarded-Proto') ||
$request->headers->has('X-Real-IP') ||
$request->headers->has('Forwarded')) {
return false;
}
// True localhost request without proxy headers
return true;
}
/**
* Handle login POST request
*/
private function handle_login(Request $request): Response
{
$password = config('rsx.gatekeeper.password');
$submitted = $request->input('password');
if ($submitted === $password) {
// Authentication successful
$cookie_name = config('rsx.gatekeeper.cookie_name', 'gatekeeper_auth');
$lifetime_hours = config('rsx.gatekeeper.cookie_lifetime_hours', 12);
$cookie_value = hash('sha256', $password);
$cookie = cookie($cookie_name, $cookie_value, 60 * $lifetime_hours);
// Redirect to originally requested URL or home
$redirect = $request->input('redirect', '/');
return redirect($redirect)->withCookie($cookie);
}
// Authentication failed - show login page with error
return $this->show_login_page($request, 'Invalid password. Please try again.');
}
/**
* Show the gatekeeper login page
*/
private function show_login_page(Request $request, string $error = null): Response
{
$data = [
'title' => config('rsx.gatekeeper.title', 'Development Preview'),
'subtitle' => config('rsx.gatekeeper.subtitle', 'This is a restricted development preview site. Please enter the access password to continue.'),
'logo' => config('rsx.gatekeeper.logo'),
'error' => $error,
'redirect' => $request->fullUrl(),
];
return response()->view('gatekeeper.login', $data, 403);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\RSpade\Core\Debug\Debugger;
class PlaywrightTestMode
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Only process Playwright headers in non-production environments
// and only from loopback addresses (no proxy headers)
if (app()->environment('production') || !is_loopback_ip()) {
return $next($request);
}
// If this is a Playwright test request, configure console debug from headers
if ($request->hasHeader('X-Playwright-Test')) {
// Configure console debug settings from headers
Debugger::configure_from_headers();
$response = $next($request);
// If this is a redirect response, modify the Location header to be relative
if ($response->isRedirect()) {
$location = $response->headers->get('Location');
// Parse the URL and extract just the path and query
$parsed = parse_url($location);
if (isset($parsed['path'])) {
$relative_url = $parsed['path'];
if (isset($parsed['query'])) {
$relative_url .= '?' . $parsed['query'];
}
if (isset($parsed['fragment'])) {
$relative_url .= '#' . $parsed['fragment'];
}
// Only modify if it's a local redirect (not external)
if (!isset($parsed['host']) || $parsed['host'] === $request->getHost() || $parsed['host'] === '127.0.0.1' || $parsed['host'] === 'localhost') {
$response->headers->set('Location', $relative_url);
}
}
}
return $response;
}
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* DISABLED: 2025-09-14
* This middleware was disabled as it appears to be a leftover from an earlier failed routing attempt.
* RSX routing is now handled through the 404 exception handler in app/Exceptions/Handler.php.
*
* This file is kept for observation in case any key functionality needs to be restored:
* - Automatic cache clearing in development mode
* - Path exclusion for certain prefixes (_debugbar, horizon, telescope, etc.)
* - Session initialization for RSX requests
* - RSX debugging headers
*
* To re-enable this middleware:
* 1. Rename file back to RsxMiddleware.php (remove .disabled extension)
* 2. Re-add to app/Http/Kernel.php:
* protected $routeMiddleware = [
* 'rsx' => \App\Http\Middleware\RsxMiddleware::class,
* ];
*
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
/**
* RsxMiddleware processes requests before RSX dispatch
*
* This middleware:
* - Checks if request should be handled by RSX
* - Maintains session and auth state
* - Adds RSX-specific headers
*/
class RsxMiddleware
{
/**
* Handle an incoming request
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// In development mode, automatically disable all Laravel caches
if (!app()->environment('production')) {
$this->ensure_caches_disabled();
}
// Note: Asset requests are handled by the RSX AssetHandler in the Dispatcher
// No need to block them here
// Check if request path should be excluded from RSX
if ($this->is_excluded_path($request)) {
abort(404);
}
// Add RSX indicator to request
$request->attributes->set('rsx_request', true);
// Ensure session is started for RSX requests
if ($request->hasSession()) {
$request->session()->start();
}
// Process the request
$response = $next($request);
// Add RSX response headers if configured
if (config('rsx.development.show_route_details', false)) {
$response->headers->set('X-RSX-Dispatch', 'true');
// Add dispatch time if available
if ($request->attributes->has('rsx_dispatch_time')) {
$response->headers->set(
'X-RSX-Dispatch-Time',
$request->attributes->get('rsx_dispatch_time') . 'ms'
);
}
}
return $response;
}
/**
* Check if path should be excluded from RSX routing
*
* @param Request $request
* @return bool
*/
protected function is_excluded_path(Request $request)
{
$path = $request->path();
// Never handle Laravel's default routes
$excluded_prefixes = [
'_debugbar',
'horizon',
'telescope',
'nova',
'livewire',
'sanctum',
'broadcasting'
];
foreach ($excluded_prefixes as $prefix) {
if (str_starts_with($path, $prefix . '/') || $path === $prefix) {
return true;
}
}
// Check custom excluded paths from config
$custom_excluded = config('rsx.routing.excluded_paths', []);
foreach ($custom_excluded as $excluded) {
if (str_starts_with($path, $excluded)) {
return true;
}
}
return false;
}
/**
* Ensure all Laravel caches are disabled in development mode
*
* @return void
*/
protected function ensure_caches_disabled()
{
static $caches_checked = false;
// Only check once per request lifecycle
if ($caches_checked) {
return;
}
$caches_checked = true;
// Check if any caches exist that shouldn't in development
$cache_files = [
'config' => app()->getCachedConfigPath(),
'routes' => app()->getCachedRoutesPath(),
'events' => app()->getCachedEventsPath(),
];
$files = app('files');
foreach ($cache_files as $type => $path) {
if ($files->exists($path)) {
// Clear the cache silently using output buffering
ob_start();
try {
switch ($type) {
case 'config':
\Artisan::call('config:clear');
break;
case 'routes':
\Artisan::call('route:clear');
break;
case 'events':
\Artisan::call('event:clear');
break;
}
} catch (\Exception $e) {
// Silently ignore any errors - we're just trying to clear caches
} finally {
ob_end_clean();
}
}
}
// Also check compiled views and clear if needed
$compiled_path = config('view.compiled');
if ($files->isDirectory($compiled_path)) {
$compiled_views = $files->glob("{$compiled_path}/*");
if (count($compiled_views) > 100) { // Only clear if there are many compiled views
ob_start();
try {
\Artisan::call('view:clear');
} catch (\Exception $e) {
// Silently ignore
} finally {
ob_end_clean();
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

0
app/Jobs/.placeholder Executable file
View File

66
app/Mail/VerificationCode.php Executable file
View File

@@ -0,0 +1,66 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class VerificationCode extends Mailable
{
use Queueable, SerializesModels;
/**
* The user instance.
*
* @var \App\Models\User
*/
public $user;
/**
* The verification code.
*
* @var string
*/
public $code;
/**
* The code expiration time in minutes.
*
* @var int
*/
public $expiresInMinutes;
/**
* Create a new message instance.
*
* @param \App\Models\User $user
* @param string $code
* @return void
*/
public function __construct(User $user, $code)
{
$this->user = $user;
$this->code = $code;
$this->expiresInMinutes = config('authentication.two_factor.sms.code_lifetime', 10);
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject('Your Verification Code')
->view('emails.verification-code');
}
}

74
app/Mail/VerifyEmail.php Executable file
View File

@@ -0,0 +1,74 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\Mail;
use App\Models\PendingRegistration;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\URL;
class VerifyEmail extends Mailable
{
use Queueable, SerializesModels;
/**
* The pending registration instance.
*
* @var \App\Models\PendingRegistration
*/
public $registration;
/**
* The verification URL.
*
* @var string
*/
public $url;
/**
* The expiration time in hours.
*
* @var int
*/
public $expiresInHours;
/**
* Create a new message instance.
*
* @param \App\Models\PendingRegistration $registration
* @return void
*/
public function __construct(PendingRegistration $registration)
{
$this->registration = $registration;
$this->url = URL::temporarySignedRoute(
'auth.verify-email',
now()->addHours(24),
['token' => $registration->verification_token]
);
$this->expiresInHours = ceil(
$registration->expires_at->diffInMinutes(now()) / 60
);
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject('Verify Your Email Address')
->view('emails.verify-email');
}
}

173
app/Models/File_Hash.php Executable file
View File

@@ -0,0 +1,173 @@
<?php
namespace App\Models;
/**
* File_Hash model representing unique physical files
*
* This model enables deduplication of file storage - multiple logical files
* can reference the same physical file if they have the same content hash.
*/
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
use Rsx\Models\File_Model;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-09-28 10:36:08
* Table: file_hashes
*
* @property int $id
* @property mixed $hash
* @property mixed $mime_type
* @property int $size
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
*
* @mixin \Eloquent
*/
class File_Hash extends Rsx_Model_Abstract
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];
/**
* _AUTO_GENERATED_ Date columns for Carbon casting
*/
protected $dates = [
'created_at',
'updated_at',
];
/**
* The table associated with the model
*
* @var string
*/
protected $table = 'file_hashes';
/**
* Column metadata for special handling
*
* @var array
*/
protected $columnMeta = [
// No special metadata needed for file_hashes table columns yet
];
/**
* Get all logical files that reference this physical file
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function files()
{
return $this->hasMany(File_Model::class, 'file_hash_id');
}
/**
* Get the storage path for this file
* Based on the hash for efficient file system distribution
*
* @return string
*/
public function get_storage_path()
{
// Split hash into subdirectories for better file system performance
// e.g., hash "abc123..." becomes "storage/files/ab/c1/abc123..."
$hash = $this->hash;
$dir1 = substr($hash, 0, 2);
$dir2 = substr($hash, 2, 2);
return "storage/files/{$dir1}/{$dir2}/{$hash}";
}
/**
* Get the full file system path
*
* @return string
*/
public function get_full_path()
{
return storage_path($this->get_storage_path());
}
/**
* Check if the physical file exists on disk
*
* @return bool
*/
public function file_exists()
{
return file_exists($this->get_full_path());
}
/**
* Get human-readable file size
*
* @return string
*/
public function get_human_size()
{
$bytes = $this->size;
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
/**
* Find or create a file hash record
*
* @param string $hash
* @param string $mime_type
* @param int $size
* @return static
*/
public static function find_or_create($hash, $mime_type, $size)
{
$file_hash = static::where('hash', $hash)->first();
if (!$file_hash) {
$file_hash = new static();
$file_hash->hash = $hash;
$file_hash->mime_type = $mime_type;
$file_hash->size = $size;
$file_hash->save();
}
return $file_hash;
}
/**
* Calculate hash for file content
*
* @param string $content
* @return string
*/
public static function calculate_hash($content)
{
return hash('sha256', $content);
}
/**
* Calculate hash for a file path
*
* @param string $file_path
* @return string|false
*/
public static function calculate_file_hash($file_path)
{
if (!file_exists($file_path)) {
return false;
}
return hash_file('sha256', $file_path);
}
}

47
app/Models/FlashAlert.php Executable file
View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-09-28 10:36:08
* Table: flash_alerts
*
* @property int $id
* @property mixed $session_id
* @property string $message
* @property mixed $class_attribute
* @property string $created_at
* @property int $created_by
* @property int $updated_by
* @property string $updated_at
*
* @mixin \Eloquent
*/
class FlashAlert extends Rsx_Model_Abstract
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];
/**
* _AUTO_GENERATED_ Date columns for Carbon casting
*/
protected $dates = [
'created_at',
'updated_at',
];
protected $table = 'flash_alerts';
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime',
];
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class SystemNotification extends Notification implements ShouldQueue
{
use Queueable;
protected $title;
protected $message;
protected $actionUrl;
protected $actionText;
protected $includeGreeting;
/**
* Create a new notification instance.
*
* @param string $title
* @param string $message
* @param string|null $actionUrl
* @param string|null $actionText
* @param bool $includeGreeting
* @return void
*/
public function __construct(
string $title,
string $message,
string $actionUrl = null,
string $actionText = null,
bool $includeGreeting = true
) {
$this->title = $title;
$this->message = $message;
$this->actionUrl = $actionUrl;
$this->actionText = $actionText;
$this->includeGreeting = $includeGreeting;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$mail = (new MailMessage)
->subject($this->title)
->line($this->message);
if ($this->includeGreeting) {
$mail->greeting('Hello ' . $notifiable->name . '!');
}
if ($this->actionUrl && $this->actionText) {
$mail->action($this->actionText, $this->actionUrl);
}
$mail->line('Thank you for using DMR Bridge!');
return $mail;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'title' => $this->title,
'message' => $this->message,
'action_url' => $this->actionUrl,
'action_text' => $this->actionText,
];
}
}

0
app/Policies/.placeholder Executable file
View File

View File

@@ -0,0 +1,215 @@
<?php
namespace App\Providers;
use App\RSpade\Core\Providers\Rsx_Preboot_Service;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\ViewErrorBag;
use Log;
class AppServiceProvider extends ServiceProvider
{
/**
* Query logging modes
*/
public const QUERY_LOG_NONE = 0; // Default - no logging
public const QUERY_LOG_ALL_STDOUT = 1; // Log all queries to stdout
public const QUERY_LOG_DESTRUCTIVE_STDOUT = 2; // Log only destructive queries to stdout
public const QUERY_LOG_ALL_LARAVEL = 3; // Log all queries to Laravel log
public const QUERY_LOG_DESTRUCTIVE_LARAVEL = 4; // Log only destructive queries to Laravel log
/**
* Current query logging mode
*
* @var int
*/
protected static $query_log_mode = self::QUERY_LOG_NONE;
/**
* Enable query echoing (backwards compatibility - sets ALL_STDOUT mode)
*
* @return void
*/
public static function enable_query_echo()
{
self::$query_log_mode = self::QUERY_LOG_ALL_STDOUT;
}
/**
* Disable query echoing
*
* @return void
*/
public static function disable_query_echo()
{
self::$query_log_mode = self::QUERY_LOG_NONE;
}
/**
* Set query logging mode
*
* @param int $mode One of the QUERY_LOG_* constants
* @return void
*/
public static function set_query_log_mode(int $mode)
{
self::$query_log_mode = $mode;
}
/**
* Check if a query is destructive (modifies data)
*
* @param string $sql
* @return bool
*/
protected static function is_destructive_query(string $sql): bool
{
$sql_upper = strtoupper(trim($sql));
// Check for destructive SQL operations
$destructive_patterns = [
'INSERT ',
'UPDATE ',
'DELETE ',
'ALTER ',
'CREATE ',
'DROP ',
'TRUNCATE ',
'REPLACE ',
'RENAME ',
'MODIFY ',
'ADD COLUMN',
'DROP COLUMN',
'ADD INDEX',
'DROP INDEX',
'ADD CONSTRAINT',
'DROP CONSTRAINT',
];
foreach ($destructive_patterns as $pattern) {
if (strpos($sql_upper, $pattern) === 0) {
return true;
}
}
return false;
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// Initialize RSpade pre-boot services (debug locks, cache clearing, trace mode)
Rsx_Preboot_Service::init();
// Load JQHTML Laravel Bridge from node_modules
$jqhtmlBridge = base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
if (file_exists($jqhtmlBridge)) {
require_once $jqhtmlBridge;
}
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Override Ignition's exception renderer with our custom one that fixes multi-line display
if (!app()->environment('production')) {
$this->app->bind(
'Illuminate\Contracts\Foundation\ExceptionRenderer',
fn ($app) => $app->make(\App\RSpade\Integrations\Jqhtml\JqhtmlExceptionRenderer::class)
);
}
// Configure MySQL connection to use custom grammar with millisecond precision
$connection = DB::connection();
if ($connection->getDriverName() === 'mysql') {
$connection->setQueryGrammar(new \App\RSpade\Core\Database\Query\Grammars\Query_MySqlGrammar());
$connection->setSchemaGrammar(new \App\RSpade\Core\Database\Schema\Grammars\Schema_MySqlGrammar());
}
// Set up query listener for migration/normalize debugging
DB::listen(function ($query) {
if (self::$query_log_mode === self::QUERY_LOG_NONE) {
return;
}
$sql = $query->sql;
$is_destructive = self::is_destructive_query($sql);
switch (self::$query_log_mode) {
case self::QUERY_LOG_ALL_STDOUT:
echo $sql . "\n";
break;
case self::QUERY_LOG_DESTRUCTIVE_STDOUT:
if ($is_destructive) {
echo $sql . "\n";
}
break;
case self::QUERY_LOG_ALL_LARAVEL:
Log::debug('SQL Query: ' . $sql);
break;
case self::QUERY_LOG_DESTRUCTIVE_LARAVEL:
if ($is_destructive) {
Log::debug('SQL Query (Destructive): ' . $sql);
}
break;
}
});
// Share $errors variable with all views for @error directive
// Disabled for custom session handler
// View::composer('*', function ($view) {
// $view->with('errors', session()->get('errors', new ViewErrorBag));
// });
// Register custom database session handler to preserve user_id and site_id columns
// Disabled for custom session handler
// Session::extend('database', function ($app) {
// $connection = $app['db']->connection($app['config']['session.connection']);
// $table = $app['config']['session.table'];
// $lifetime = $app['config']['session.lifetime'];
//
// return new \App\RSpade\Core\Session\Custom_DatabaseSessionHandler(
// $connection,
// $table,
// $lifetime,
// $app
// );
// });
// Override cache:clear to integrate RSX clearing
if ($this->app->runningInConsole()) {
$this->override_cache_clear();
}
}
/**
* Override Laravel's cache:clear command to integrate with RSX
*
* @return void
*/
protected function override_cache_clear()
{
// Only override cache:clear to also clear RSX caches
$this->app->extend('command.cache.clear', function ($command, $app) {
return new \App\Console\Commands\CacheClearCommand();
});
}
}

26
app/Providers/CLAUDE.md Executable file
View File

@@ -0,0 +1,26 @@
# Providers Directory
This CLAUDE.md file contains a brief synopsis of the purpose of this directory, then a list of files in this directory with the file sizes of each file, and a short description and relevant key points of information for every file which is important in this directory. Unimportant files like images or temporary data directories are not listed in this file. When visiting this directory, the AI agent is instructed to do an ls on the directory to get the directory contents and file sizes - and if the file size diverges from the size in CLAUDE.md, that means the file has changed, and the description in CLAUDE.md is not up to date. This doesn't trigger this to be regenerated immediately, but let's say we wanted to know about a specific file by viewing CLAUDE.md and we discovered it was out of date, we would need to reread and update the documentation for that file in the CLAUDE.md at that time before we considered any details about it. CLAUDE.md might also contain other bits of information that is critical to know if you are looking at notes in the directory where the CLAUDE.md file lives.
## Directory Purpose
The Providers directory contains Laravel service providers which are the central configuration mechanism in Laravel applications. Service providers bootstrap the application by binding services in the service container, registering events, middleware, routes, and configuration.
## File Index
| File | Size | Description |
|------|------|-------------|
| AppServiceProvider.php | 940 bytes | General application service provider that refreshes session lifetime with each page request and shares constants (states and date formats) with all views. |
| AuthServiceProvider.php | 751 bytes | Authentication provider that defines model-to-policy mappings for authorization, registers Bridge and Talkgroup policies, and calls registerPolicies() to load these mappings. |
| BroadcastServiceProvider.php | 380 bytes | Broadcasting provider that registers routes for broadcasting and loads the routes/channels.php file for channel authorization. |
| EventServiceProvider.php | 926 bytes | Event handler provider that maps the Registered event to SendEmailVerificationNotification listener and disables automatic event discovery. |
| RecaptchaServiceProvider.php | 1,383 bytes | Custom provider for Google reCAPTCHA integration that adds a 'recaptcha' validator, skips validation in testing environment, and verifies reCAPTCHA responses via Google's API. |
| RouteServiceProvider.php | 1,452 bytes | Route configuration provider that defines HOME constant for redirection after authentication, configures rate limiting for API routes, and loads API, multi-tenant, and RSpade routes with appropriate middleware. |
## Implementation Notes
1. All service providers follow the Laravel provider pattern with register() and boot() methods.
2. The register() method happens before all providers are loaded, while boot() happens after all providers are loaded.
3. Custom providers like RecaptchaServiceProvider extend Laravel's base ServiceProvider.
4. The RouteServiceProvider has been extended to support multi-tenant and RSpade route files.
5. AppServiceProvider handles session management and constants sharing with views.

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
$this->routes(function () {
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\RSpade\Bundles;
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
/**
* Bootstrap 5 CDN Bundle
*
* Provides Bootstrap 5 CSS and JavaScript via CDN.
*/
class Bootstrap5_Bundle extends Rsx_Bundle_Abstract
{
/**
* Define the bundle configuration
*
* @return array Bundle configuration
*/
public static function define(): array
{
return [
'include' => [], // No local files
'cdn_assets' => [
'css' => [
[
'url' => 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/css/bootstrap.min.css',
'integrity' => 'sha512-SbiR/eusphKoMVVXysTKG/7VseWii+Y3FdHrt0EpKgpToZeemhqHeZeLWLhJutz/2ut2Vw1uQEj2MbRF+TVBUA==',
],
],
'js' => [
[
'url' => 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.bundle.min.js',
'integrity' => 'sha512-i9cEfJwUwViEPFKdC1enz4ZRGBj8YQo6QByFTF92YXHi7waCqyexvRD75S5NVTsSiTv7rKWqG9Y5eFxmRsOn0A==',
],
],
],
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\RSpade\Bundles;
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
/**
* jQuery CDN Bundle
*
* Provides jQuery library via CDN. This bundle is automatically included
* in all other bundles as a required dependency.
*/
class Jquery_Bundle extends Rsx_Bundle_Abstract
{
/**
* Define the bundle configuration
*
* @return array Bundle configuration
*/
public static function define(): array
{
return [
'include' => [], // No local files
'cdn_assets' => [
'js' => [
[
'url' => 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js',
'integrity' => 'sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ==',
],
],
],
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\RSpade\Bundles;
use App\RSpade\Core\Bundle\Rsx_Bundle_Abstract;
/**
* Lodash CDN Bundle
*
* Provides Lodash utility library via CDN. This bundle is automatically included
* in all other bundles as a required dependency.
*/
class Lodash_Bundle extends Rsx_Bundle_Abstract
{
/**
* Define the bundle configuration
*
* @return array Bundle configuration
*/
public static function define(): array
{
return [
'include' => [], // No local files
'cdn_assets' => [
'js' => [
[
'url' => 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
'integrity' => 'sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==',
],
],
],
];
}
}

325
app/RSpade/CodeQuality/CLAUDE.md Executable file
View File

@@ -0,0 +1,325 @@
# RSpade Code Quality System
## Overview
The Code Quality system is a modular, extensible framework for enforcing coding standards and best practices across the RSpade codebase. It replaces a monolithic 1921-line checker with a clean, maintainable architecture using Manifest-based auto-discovery.
## Architecture
### Core Components
1. **CodeQualityChecker** (`CodeQualityChecker.php`)
- Main orchestrator that discovers and runs all rules
- Auto-discovers rules via RuleDiscovery::discover_rules()
- Handles file scanning, caching, and violation collection
- Performs syntax linting for PHP, JavaScript, and JSON files
2. **CodeQualityRule_Abstract** (`Rules/CodeQualityRule_Abstract.php`)
- Base class for all code quality rules
- Defines the interface: `get_id()`, `get_name()`, `check()`, etc.
- Provides `add_violation()` helper method
- Rules self-register by extending this class
3. **Violation** (`Violation.php`)
- Data class representing a code violation
- Contains: rule_id, file_path, line_number, message, severity, code_snippet, suggestion
- Provides `to_array()` for serialization
### Support Classes
- **ViolationCollector** - Aggregates violations from all rules
- **CacheManager** - Caches sanitized file contents to improve performance
- **FileSanitizer** - Removes comments and strings for accurate code analysis
## Rule Categories
### PHP Rules (`Rules/PHP/`)
1. **NamingConventionRule** (PHP-NAMING-01)
- Enforces underscore_case for methods and variables
- Excludes Laravel framework methods (toArray, firstOrCreate, etc.)
- Severity: Medium
2. **MassAssignmentRule** (PHP-MASS-01)
- Prohibits use of $fillable property
- Ensures $guarded = ['*'] or removal
- Severity: High
3. **PhpFallbackLegacyRule** (PHP-FALLBACK-01)
- Detects "fallback" or "legacy" in comments/function names
- Enforces fail-loud principle
- Severity: Critical
4. **DbTableUsageRule** (PHP-DB-01)
- Prohibits DB::table() usage
- Requires ORM models for database access
- Severity: High
5. **FunctionExistsRule** (PHP-FUNC-01)
- Prohibits function_exists() checks
- Enforces predictable runtime environment
- Severity: High
### Jqhtml Rules (`Rules/Jqhtml/`)
1. **JqhtmlInlineScriptRule** (JQHTML-INLINE-01)
- Prohibits inline <script> and <style> tags in .jqhtml templates
- Enforces component class pattern with Jqhtml_Component
- Requires separate .js and .scss files
- Severity: Critical
- Runs at manifest-time
### JavaScript Rules (`Rules/JavaScript/`)
1. **VarUsageRule** (JS-VAR-01)
- Prohibits 'var' keyword, requires let/const
- Severity: Medium
2. **DefensiveCodingRule** (JS-DEFENSIVE-01)
- Prohibits typeof checks for core classes
- Core classes always exist in runtime
- Severity: High
3. **InstanceMethodsRule** (JS-STATIC-01)
- Enforces static methods in JavaScript classes
- Exceptions allowed with @instance-class comment
- Severity: Medium
4. **JQueryUsageRule** (JS-JQUERY-01)
- Enforces $ over jQuery
- Detects deprecated methods (live, die, bind, etc.)
- Severity: Medium
5. **ThisUsageRule** (JS-THIS-01)
- Detects problematic 'this' usage
- Suggests class reference pattern
- Severity: Medium
6. **DocumentReadyRule** (JS-READY-01)
- Prohibits jQuery ready patterns
- Requires ES6 class with static init()
- Severity: High
7. **JsFallbackLegacyRule** (JS-FALLBACK-01)
- JavaScript version of fallback/legacy detection
- Severity: Critical
### Common Rules (`Rules/Common/`)
1. **FilenameCaseRule** (FILE-CASE-01)
- Enforces lowercase filenames
- Severity: Low
2. **FilenameEnhancedRule** (FILE-NAME-01)
- Validates controller/model naming conventions
- Checks file-class name consistency
- Severity: Medium
3. **RootFilesRule** (FILE-ROOT-01)
- Restricts files in project root
- Maintains clean project structure
- Severity: Medium
4. **RsxTestFilesRule** (FILE-RSX-01)
- Prevents test files directly in rsx/
- Enforces proper test organization
- Severity: Medium
5. **RouteExistsRule** (ROUTE-EXISTS-01)
- Validates Rsx::Route() calls reference existing routes
- Checks controller/method combinations exist in manifest
- Suggests placeholder URLs for unimplemented routes
- Severity: High
### Sanity Check Rules (`Rules/SanityChecks/`)
1. **PhpSanityCheckRule** (PHP-SC-001)
- Complex pattern detection (currently disabled)
- Detects suspicious code patterns
- Severity: Critical
## Configuration
### Config File (`config/rsx.php`)
```php
'code_quality' => [
'enabled' => env('CODE_QUALITY_ENABLED', true),
'cache_enabled' => true,
'parallel_processing' => false,
'excluded_directories' => [
'vendor',
'node_modules',
'storage',
'bootstrap/cache',
'CodeQuality', // Exclude checker itself
],
'rsx_test_whitelist' => [
// Files allowed in rsx/ directory
'main.php',
'routes.php',
],
],
```
### Disabling Rules
Rules can be disabled by adding them to the disabled list:
```php
'disabled_rules' => [
'PHP-SC-001', // Temporarily disabled
],
```
## Usage
### Command Line
```bash
# Run all checks
php artisan rsx:check
# Check specific directory
php artisan rsx:check rsx/
# Check specific file
php artisan rsx:check app/Models/User.php
```
### Exception Comments
Add exception comments to bypass specific violations:
```php
// @RULE-ID-EXCEPTION (e.g., @PHP-NAMING-01-EXCEPTION)
// Code that would normally violate rules
```
## Development
### Creating New Rules
1. Create a new class extending `CodeQualityRule_Abstract`
2. Place in appropriate Rules subdirectory
3. Implement required methods:
- `get_id()` - Unique rule identifier
- `get_name()` - Human-readable name
- `check()` - Violation detection logic
4. Add to Manifest scan directories if needed
Example:
```php
namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class MyNew_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PHP-NEW-01';
}
public function get_name(): string
{
return 'My New Rule';
}
public function get_description(): string
{
return 'Description of what this rule checks';
}
public function get_file_patterns(): array
{
return ['*.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; // Always false unless explicitly approved
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Detection logic
if ($violation_found) {
$this->add_violation(
$file_path,
$line_number,
"Violation message",
$code_snippet,
"How to fix",
'medium'
);
}
}
}
```
### Testing Rules
1. Create a temporary test file with violations
2. Run `php artisan rsx:check`
3. Verify violations are detected correctly
4. Clean up test files
## Migration from Monolith
The original 1921-line `CodeStandardsChecker.php` has been:
1. Archived to `/archived/CodeStandardsChecker.old.php`
2. Split into modular rule classes
3. Enhanced with auto-discovery via Manifest
4. Improved with better caching and performance
All original rule logic has been preserved exactly, ensuring no regression in code quality checks.
## Performance
- **Caching**: Sanitized file contents are cached to avoid repeated processing
- **Incremental Linting**: Files are only linted if changed since last check
- **Efficient Scanning**: Smart directory traversal skips excluded paths
## Manifest-Time Checking
By default, code quality rules run only when `php artisan rsx:check` is executed. However, certain critical rules can be configured to run during manifest builds to provide immediate feedback.
### When to Enable Manifest-Time Checking
**DO NOT** enable manifest-time checking unless you have explicit approval. Rules should only run at manifest-time if they:
1. Enforce critical framework conventions that would break the application
2. Need to provide immediate feedback before code execution
3. Have been specifically requested by framework maintainers
### Current Manifest-Time Rules
Only the following rules are approved for manifest-time execution:
- **BLADE-SCRIPT-01** (InlineScriptRule): Prevents inline JavaScript in Blade files (critical architecture violation)
- **JQHTML-INLINE-01** (JqhtmlInlineScriptRule): Prevents inline scripts/styles in Jqhtml template files (critical architecture violation)
All other rules should return `false` from `is_called_during_manifest_scan()`.
## Severity Levels
- **Critical**: Must fix immediately (e.g., fallback code)
- **High**: Should fix soon (e.g., mass assignment)
- **Medium**: Fix when convenient (e.g., naming conventions)
- **Low**: Minor issues (e.g., filename case)

View File

@@ -0,0 +1,553 @@
<?php
namespace App\RSpade\CodeQuality;
use App\RSpade\CodeQuality\CodeQuality_Violation;
use App\RSpade\CodeQuality\Support\CacheManager;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\ViolationCollector;
use App\RSpade\Core\Manifest\Manifest;
class CodeQualityChecker
{
protected static ?ViolationCollector $collector = null;
protected static ?CacheManager $cache_manager = null;
protected static array $rules = [];
protected static array $config = [];
public static function init(array $config = []): void
{
static::$collector = new ViolationCollector();
static::$cache_manager = new CacheManager();
static::$config = $config;
// Load all rules via auto-discovery
static::load_rules();
// Clean up old NPM bundle files on initialization
static::_cleanup_old_npm_bundles();
}
/**
* Clean up old NPM bundle files
* NPM bundles are cached based on package-lock.json + npm array + CWD
* Old bundles from different cache keys should be removed
*/
protected static function _cleanup_old_npm_bundles(): void
{
$bundle_dir = storage_path('rsx-build/bundles');
// Skip if directory doesn't exist yet
if (!is_dir($bundle_dir)) {
return;
}
// Find all npm_*.js files
$npm_bundles = glob($bundle_dir . '/npm_*.js');
if (empty($npm_bundles)) {
return;
}
// Keep the most recent 5 npm bundle files per bundle name
// Group by bundle name (npm_<bundlename>_<hash>.js)
$bundles_by_name = [];
foreach ($npm_bundles as $file) {
$filename = basename($file);
// Extract bundle name from npm_<bundlename>_<hash>.js
if (preg_match('/^npm_([^_]+)_[a-f0-9]{32}\.js$/', $filename, $matches)) {
$bundle_name = $matches[1];
if (!isset($bundles_by_name[$bundle_name])) {
$bundles_by_name[$bundle_name] = [];
}
$bundles_by_name[$bundle_name][] = [
'file' => $file,
'mtime' => filemtime($file)
];
}
}
// For each bundle name, keep only the 5 most recent files
foreach ($bundles_by_name as $bundle_name => $files) {
// Sort by modification time, newest first
usort($files, function($a, $b) {
return $b['mtime'] - $a['mtime'];
});
// Delete all but the most recent 5
$to_keep = 5;
for ($i = $to_keep; $i < count($files); $i++) {
@unlink($files[$i]['file']);
}
}
}
/**
* Load all rules via shared discovery logic
*/
protected static function load_rules(): void
{
// Check if we should exclude manifest-time rules (e.g., when running from rsx:check)
$exclude_manifest_time_rules = static::$config['exclude_manifest_time_rules'] ?? false;
// Use shared rule discovery that doesn't require manifest
static::$rules = Support\RuleDiscovery::discover_rules(
static::$collector,
static::$config,
false, // Get all rules, not just manifest scan ones
$exclude_manifest_time_rules // Exclude manifest-time rules if requested
);
}
/**
* Check a single file
*/
public static function check_file(string $file_path): void
{
// Get excluded directories from config
$excluded_dirs = config('rsx.manifest.excluded_dirs', ['vendor', 'node_modules', 'storage', '.git', 'public', 'resource']);
// Check if file is in any excluded directory
foreach ($excluded_dirs as $excluded_dir) {
if (str_contains($file_path, '/' . $excluded_dir . '/')) {
return;
}
}
// Skip CodeQuality infrastructure files, but allow checking Rules directory
// This enables meta rules to check other rules for code quality violations
if (str_contains($file_path, '/app/RSpade/CodeQuality/') &&
!str_contains($file_path, '/app/RSpade/CodeQuality/Rules/')) {
return;
}
// Get file extension
$extension = pathinfo($file_path, PATHINFO_EXTENSION);
// Check for syntax errors first
if ($extension === 'php') {
if (static::lint_php_file($file_path)) {
// Syntax error found, don't run other checks
return;
}
} elseif (in_array($extension, ['js', 'jsx', 'ts', 'tsx'])) {
if (static::lint_javascript_file($file_path)) {
// Syntax error found, don't run other checks
return;
}
} elseif ($extension === 'json') {
if (static::lint_json_file($file_path)) {
// Syntax error found, don't run other checks
return;
}
}
// Get cached sanitized file if available
$cached_data = static::$cache_manager->get_sanitized_file($file_path);
if ($cached_data === null) {
// Sanitize the file
$sanitized_data = FileSanitizer::sanitize($file_path);
// Cache the sanitized data
static::$cache_manager->set_sanitized_file($file_path, $sanitized_data);
} else {
$sanitized_data = $cached_data;
}
// Get metadata from manifest if available
try {
$metadata = Manifest::get_file($file_path) ?? [];
} catch (\Exception $e) {
$metadata = [];
}
// Check if this is a Console Command file
$is_console_command = str_contains($file_path, '/app/Console/Commands/');
// Run each rule on the file
foreach (static::$rules as $rule) {
// If this is a Console Command, only run rules that support them
if ($is_console_command && !$rule->supports_console_commands()) {
continue;
}
// Check if this rule applies to this file type
$applies = false;
foreach ($rule->get_file_patterns() as $pattern) {
if (static::matches_pattern($file_path, $pattern)) {
$applies = true;
break;
}
}
if (!$applies) {
continue;
}
// Check for rule-specific exception comment in original file content
$rule_id = $rule->get_id();
$exception_pattern = '@' . $rule_id . '-EXCEPTION';
$original_content = file_get_contents($file_path);
if (str_contains($original_content, $exception_pattern)) {
// Skip this rule for this file
continue;
}
// Run the rule
$rule->check($file_path, $sanitized_data['content'], $metadata);
}
}
/**
* Check multiple files
*/
public static function check_files(array $file_paths): void
{
// First run special directory-level checks for rules that need them
foreach (static::$rules as $rule) {
// Check for special check_root method (RootFilesRule)
if (method_exists($rule, 'check_root')) {
$rule->check_root();
}
// Check for special check_rsx method (RsxTestFilesRule)
if (method_exists($rule, 'check_rsx')) {
$rule->check_rsx();
}
// Check for special check_required_models method (RequiredModelsRule)
if (method_exists($rule, 'check_required_models')) {
$rule->check_required_models();
}
// Check for special check_rsx_commands method (RsxCommandsDeprecatedRule)
if (method_exists($rule, 'check_rsx_commands')) {
$rule->check_rsx_commands();
}
// Check for special check_commands method (CommandOrganizationRule)
if (method_exists($rule, 'check_commands')) {
$rule->check_commands();
}
}
// Then check individual files
foreach ($file_paths as $file_path) {
static::check_file($file_path);
}
}
/**
* Check all files in a directory
*/
public static function check_directory(string $directory, bool $recursive = true): void
{
// First run special directory-level checks for rules that need them
foreach (static::$rules as $rule) {
// Check for special check_root method (RootFilesRule)
if (method_exists($rule, 'check_root')) {
$rule->check_root();
}
// Check for special check_rsx method (RsxTestFilesRule)
if (method_exists($rule, 'check_rsx')) {
$rule->check_rsx();
}
// Check for special check_required_models method (RequiredModelsRule)
if (method_exists($rule, 'check_required_models')) {
$rule->check_required_models();
}
}
// Get all files - let rules filter by extension
$files = [];
if ($recursive) {
// Use RecursiveIteratorIterator for recursive scanning
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$files[] = $file->getPathname();
}
}
} else {
// Non-recursive - just scan immediate directory
$items = glob($directory . '/*');
$files = array_filter($items, 'is_file');
}
foreach ($files as $file) {
static::check_file($file);
}
}
/**
* Get the violation collector
*/
public static function get_collector(): ViolationCollector
{
return static::$collector;
}
/**
* Get all violations
*/
public static function get_violations(): array
{
return static::$collector->get_violations_as_arrays();
}
/**
* Clear cache
*/
public static function clear_cache(): void
{
static::$cache_manager->clear();
}
/**
* Check if a file path matches a pattern
*/
protected static function matches_pattern(string $file_path, string $pattern): bool
{
// Simple glob matching for file patterns like *.php, *.js
if (strpos($pattern, '*') === 0) {
// Pattern like *.php - check file extension
$extension = substr($pattern, 1); // Remove the *
return str_ends_with($file_path, $extension);
}
// For more complex patterns, use fnmatch if available
if (function_exists('fnmatch')) {
return fnmatch($pattern, basename($file_path));
}
// Fallback to simple string matching
return str_contains($file_path, $pattern);
}
/**
* Lint PHP file (from monolith line 536)
* Returns true if syntax error found
*/
protected static function lint_php_file(string $file_path): bool
{
// Get excluded directories from config
$excluded_dirs = config('rsx.manifest.excluded_dirs', ['vendor', 'node_modules', 'storage', '.git', 'public', 'resource']);
// Check if file is in any excluded directory
foreach ($excluded_dirs as $excluded_dir) {
if (str_contains($file_path, '/' . $excluded_dir . '/')) {
return false;
}
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return false;
}
// Create cache directory for lint flags
$cache_dir = function_exists('storage_path') ? storage_path('rsx-tmp/cache/php-lint-passed') : '/var/www/html/storage/rsx-tmp/cache/php-lint-passed';
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Generate flag file path (no .php extension to avoid IDE detection)
$base_path = function_exists('base_path') ? base_path() : '/var/www/html';
$relative_path = str_replace($base_path . '/', '', $file_path);
$flag_path = $cache_dir . '/' . str_replace('/', '_', $relative_path) . '.lintpass';
// Check if lint was already passed
if (file_exists($flag_path)) {
$source_mtime = filemtime($file_path);
$flag_mtime = filemtime($flag_path);
if ($flag_mtime >= $source_mtime) {
// File hasn't changed since last successful lint
return false; // No errors
}
}
// Run PHP lint check
$command = sprintf('php -l %s 2>&1', escapeshellarg($file_path));
$output = shell_exec($command);
// Check if there's a syntax error
if (!str_contains($output, 'No syntax errors detected')) {
// Delete flag file if it exists (file now has errors)
if (file_exists($flag_path)) {
unlink($flag_path);
}
// Just capture the error as-is
static::$collector->add(
new CodeQuality_Violation(
'PHP-SYNTAX',
$file_path,
0,
trim($output),
'critical',
null,
'Fix the PHP syntax error before running other checks.'
)
);
return true; // Error found
}
// Create flag file to indicate successful lint
touch($flag_path);
return false; // No errors
}
/**
* Lint JavaScript file (from monolith line 602)
* Returns true if syntax error found
*/
protected static function lint_javascript_file(string $file_path): bool
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return false;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return false;
}
// Skip VS Code extension directory
if (str_contains($file_path, '/resource/vscode_extension/')) {
return false;
}
// Create cache directory for lint flags
$cache_dir = function_exists('storage_path') ? storage_path('rsx-tmp/cache/js-lint-passed') : '/var/www/html/storage/rsx-tmp/cache/js-lint-passed';
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Generate flag file path (no .js extension to avoid IDE detection)
$base_path = function_exists('base_path') ? base_path() : '/var/www/html';
$relative_path = str_replace($base_path . '/', '', $file_path);
$flag_path = $cache_dir . '/' . str_replace('/', '_', $relative_path) . '.lintpass';
// Check if lint was already passed
if (file_exists($flag_path)) {
$source_mtime = filemtime($file_path);
$flag_mtime = filemtime($flag_path);
if ($flag_mtime >= $source_mtime) {
// File hasn't changed since last successful lint
return false; // No errors
}
}
// Run JavaScript syntax check using Node.js
$linter_path = $base_path . '/bin/js-linter.js';
if (!file_exists($linter_path)) {
// Linter script not found, skip linting
return false;
}
$command = sprintf('node %s %s 2>&1', escapeshellarg($linter_path), escapeshellarg($file_path));
$output = shell_exec($command);
// Check if there's a syntax error
if ($output && trim($output) !== '') {
// Delete flag file if it exists (file now has errors)
if (file_exists($flag_path)) {
unlink($flag_path);
}
// Parse error message for line number if available
$line_number = 0;
if (preg_match('/Line (\d+)/', $output, $matches)) {
$line_number = (int)$matches[1];
}
static::$collector->add(
new CodeQuality_Violation(
'JS-SYNTAX',
$file_path,
$line_number,
trim($output),
'critical',
null,
'Fix the JavaScript syntax error before running other checks.'
)
);
return true; // Error found
}
// Create flag file to indicate successful lint
touch($flag_path);
return false; // No errors
}
/**
* Lint JSON file (from monolith line 684)
* Returns true if syntax error found
*/
protected static function lint_json_file(string $file_path): bool
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return false;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return false;
}
// Skip VS Code extension directory
if (str_contains($file_path, '/resource/vscode_extension/')) {
return false;
}
$content = file_get_contents($file_path);
// Try to decode the JSON
json_decode($content);
// Check for JSON errors
if (json_last_error() !== JSON_ERROR_NONE) {
$error_message = json_last_error_msg();
// Try to find line number for common errors
$line_number = 0;
if (str_contains($error_message, 'Syntax error')) {
// Count lines up to the error position if possible
$lines = explode("\n", $content);
$line_number = count($lines); // Default to last line
}
static::$collector->add(
new CodeQuality_Violation(
'JSON-SYNTAX',
$file_path,
$line_number,
"JSON parse error: {$error_message}",
'critical',
null,
'Fix the JSON syntax error. Common issues: missing commas, trailing commas, unquoted keys.'
)
);
return true; // Error found
}
return false; // No errors
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\RSpade\CodeQuality;
#[Instantiatable]
class CodeQuality_Violation
{
public function __construct(
public readonly string $rule_id,
public readonly string $file_path,
public readonly int $line_number,
public readonly string $message,
public readonly string $severity,
public readonly ?string $code_snippet = null,
public readonly ?string $suggestion = null
) {}
public function to_array(): array
{
// Return in format expected by InspectCommand
return [
'file' => $this->file_path,
'line' => $this->line_number,
'type' => $this->rule_id,
'message' => $this->message,
'resolution' => $this->suggestion,
'code' => $this->code_snippet,
'severity' => $this->severity,
];
}
public function get_severity_weight(): int
{
return match($this->severity) {
'critical' => 4,
'high' => 3,
'medium' => 2,
'low' => 1,
'convention' => 0,
default => 2,
};
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace App\RSpade\CodeQuality\Parsers;
/**
* Lightweight SCSS context parser for code quality rules
*
* PURPOSE: This is NOT a full SCSS compiler/parser. It only tracks:
* - Selector nesting and building full selector paths
* - Property declarations within each selector context
* - Pseudo-state detection (:hover, :focus, :active)
*
* DESIGN: Simple state machine that builds selector context by tracking braces
* and nesting. Designed for code quality rules that need to understand what
* properties are set in hover/focus states vs base states.
*
* USAGE:
* $contexts = ScssContextParser::parse_contexts($scss_content);
* foreach ($contexts as $context) {
* if (ScssContextParser::is_in_hover_context($context['selector'])) {
* // Check properties...
* }
* }
*
* FUTURE: Can be extended to track @media queries, mixins, or other SCSS
* features as needed by new rules. Each code quality rule documents what
* parsing features it requires.
*/
class ScssContextParser
{
/**
* Parse SCSS content into selector contexts with their properties
*
* @param string $scss Raw SCSS content
* @return array Array of contexts, each with:
* - 'line': Line number where selector starts
* - 'selector': Full selector path (e.g., '.nav-link:hover')
* - 'properties': Associative array of property => value
* - 'is_hover': Boolean if selector contains :hover
* - 'is_focus': Boolean if selector contains :focus
* - 'is_active': Boolean if selector contains :active
*/
public static function parse_contexts(string $scss): array
{
$lines = explode("\n", $scss);
$contexts = [];
$selector_stack = [];
$current_context = null;
$brace_depth = 0;
$in_comment = false;
for ($i = 0; $i < count($lines); $i++) {
$line = $lines[$i];
$line_num = $i + 1;
$trimmed = trim($line);
// Skip empty lines
if (empty($trimmed)) {
continue;
}
// Handle multi-line comments
if (str_contains($line, '/*')) {
$in_comment = true;
}
if ($in_comment) {
if (str_contains($line, '*/')) {
$in_comment = false;
}
continue;
}
// Skip single-line comments
if (str_starts_with($trimmed, '//')) {
continue;
}
// Remove inline comments for processing
$clean_line = preg_replace('/\/\/.*$/', '', $line);
$clean_line = preg_replace('/\/\*.*?\*\//', '', $clean_line);
$trimmed_clean = trim($clean_line);
// Count braces before processing
$open_braces = substr_count($clean_line, '{');
$close_braces = substr_count($clean_line, '}');
// Handle closing braces - pop from selector stack
for ($j = 0; $j < $close_braces; $j++) {
if (!empty($selector_stack)) {
array_pop($selector_stack);
}
$brace_depth--;
// Save current context when closing its block
if ($current_context && $brace_depth < $current_context['depth']) {
$contexts[] = $current_context;
$current_context = null;
}
}
// Check if this line starts a new selector block
if ($open_braces > 0 && !empty($trimmed_clean)) {
// Extract the selector part (before the {)
$selector_part = trim(str_replace('{', '', $trimmed_clean));
// Skip @keyframes, @media, @import etc
if (str_starts_with($selector_part, '@')) {
$brace_depth += $open_braces;
continue;
}
// Build full selector path
$full_selector = self::build_selector_path($selector_stack, $selector_part);
// Push to stack for nested selectors
$selector_stack[] = $selector_part;
$brace_depth += $open_braces;
// Create new context
$current_context = [
'line' => $line_num,
'selector' => $full_selector,
'properties' => [],
'depth' => $brace_depth,
'is_hover' => str_contains($full_selector, ':hover'),
'is_focus' => str_contains($full_selector, ':focus'),
'is_active' => str_contains($full_selector, ':active')
];
} elseif ($open_braces > 0) {
// Opening brace without selector (continuation from previous line)
$brace_depth += $open_braces;
}
// Parse property declarations within current context
if ($current_context && $brace_depth === $current_context['depth']) {
if (preg_match('/^\s*([a-z-]+)\s*:\s*(.+?);?\s*$/i', $trimmed_clean, $matches)) {
$property = $matches[1];
$value = trim($matches[2], '; ');
$current_context['properties'][$property] = $value;
}
}
}
// Save any remaining context
if ($current_context) {
$contexts[] = $current_context;
}
return $contexts;
}
/**
* Build full selector path from selector stack
* Handles SCSS & parent reference properly
*/
private static function build_selector_path(array $stack, string $current): string
{
if (empty($stack)) {
return $current;
}
$parent = implode(' ', $stack);
// Handle & parent reference
if (str_starts_with($current, '&')) {
// Replace & with the immediate parent (last item in stack)
$immediate_parent = end($stack);
$current = str_replace('&', '', $current);
// Remove last item and rebuild
$stack_without_last = array_slice($stack, 0, -1);
if (empty($stack_without_last)) {
return $immediate_parent . $current;
}
return implode(' ', $stack_without_last) . ' ' . $immediate_parent . $current;
}
// Handle nested selectors without &
return $parent . ' ' . $current;
}
/**
* Check if a selector represents a hover/focus/active state
*/
public static function is_in_hover_context(string $selector): bool
{
return str_contains($selector, ':hover') ||
str_contains($selector, ':focus') ||
str_contains($selector, ':active');
}
/**
* Get the base selector without pseudo-states
* Example: '.btn:hover' => '.btn'
*/
public static function get_base_selector(string $selector): string
{
return preg_replace('/:(hover|focus|active|visited|disabled)/', '', $selector);
}
/**
* Compare properties between two contexts to find differences
* Useful for detecting redundant declarations or actual changes
*/
public static function compare_properties(array $base_props, array $state_props): array
{
$differences = [
'added' => [],
'changed' => [],
'same' => [],
'removed' => []
];
foreach ($state_props as $prop => $value) {
if (!isset($base_props[$prop])) {
$differences['added'][$prop] = $value;
} elseif ($base_props[$prop] !== $value) {
$differences['changed'][$prop] = [
'from' => $base_props[$prop],
'to' => $value
];
} else {
$differences['same'][$prop] = $value;
}
}
foreach ($base_props as $prop => $value) {
if (!isset($state_props[$prop])) {
$differences['removed'][$prop] = $value;
}
}
return $differences;
}
/**
* Check if a property is position/size related
* These are typically prohibited in hover states
*/
public static function is_position_property(string $property): bool
{
$position_properties = [
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'top', 'right', 'bottom', 'left',
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
'font-size', 'line-height', 'letter-spacing', 'word-spacing'
];
return in_array($property, $position_properties);
}
/**
* Check if a property is visual-only (safe for hover)
* These don't affect layout or position
*/
public static function is_visual_only_property(string $property): bool
{
$visual_properties = [
'color', 'background-color', 'background', 'background-image',
'opacity', 'visibility',
'border-color', 'outline', 'outline-color',
'text-decoration', 'text-decoration-color',
'box-shadow', 'text-shadow',
'filter', 'backdrop-filter',
'cursor'
];
// Check for exact match or if property starts with one of these
foreach ($visual_properties as $visual_prop) {
if ($property === $visual_prop || str_starts_with($property, $visual_prop . '-')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Blade;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class BladeExtends_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'BLADE-EXTENDS-01';
}
public function get_name(): string
{
return 'Blade @extends Syntax Check';
}
public function get_description(): string
{
return "Detects incorrect @extends('rsx:: usage - should use @rsx_extends instead";
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check blade files for incorrect @extends('rsx:: usage
* The correct syntax is @rsx_extends('layout_name'), not @extends('rsx::...)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in rsx/ directory
if (!str_contains($file_path, '/rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
$lines = explode("\n", $contents);
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for @extends('rsx:: pattern (with single or double quotes)
if (preg_match('/@extends\s*\(\s*[\'"]rsx::([^\'"]+)[\'"]/', $line, $matches)) {
$layout_reference = $matches[1] ?? '';
// Build suggestion
$suggestion = "The @extends directive with 'rsx::' namespace is incorrect.\n";
$suggestion .= "Use the @rsx_extends directive instead:\n";
$suggestion .= " Replace: @extends('rsx::{$layout_reference}')\n";
$suggestion .= " With: @rsx_extends('{$layout_reference}')\n\n";
$suggestion .= "The @rsx_extends directive uses the RSX ID system to locate layouts ";
$suggestion .= "by their RSX ID rather than file paths.";
$this->add_violation(
$file_path,
$line_number,
"Incorrect use of @extends('rsx::...'). Use @rsx_extends instead.",
trim($line),
$suggestion,
'high'
);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Blade;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\Core\Manifest\Manifest;
/**
* Enforces path-agnostic class references in Blade templates by preventing direct FQCN usage
*
* Blade templates should reference RSX classes by simple name only, not by FQCN.
* Direct references like \Rsx\Models\User_Model or \App\RSpade\Core\Session\Session::method()
* are not allowed - use User_Model or Session instead.
*
* Note: use statements for Rsx\ classes ARE allowed in PHP blocks within Blade files,
* though they are unnecessary due to the autoloader.
*/
class BladeFqcnUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'BLADE-RSX-FQCN-01';
}
public function get_name(): string
{
return 'Blade RSX FQCN Usage Validator';
}
public function get_description(): string
{
return 'Prevents direct FQCN references to Rsx classes in Blade templates';
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'high';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Pattern to match \Rsx\... FQCN references
// Looks for \Rsx\ followed by class path components
// Must be preceded and followed by non-alphanumeric characters (except \ for namespace)
$pattern = '/(?<![a-zA-Z0-9])\\\\Rsx\\\\[a-zA-Z0-9_\\\\]+/';
if (!preg_match_all($pattern, $contents, $matches, PREG_OFFSET_CAPTURE)) {
return;
}
// Get manifest data once for lookups
$manifest = Manifest::get_all();
foreach ($matches[0] as $match) {
$fqcn = $match[0];
$offset = $match[1];
// Clean up the FQCN - remove leading backslash and normalize
$clean_fqcn = ltrim($fqcn, '\\');
// Extract simple class name from FQCN
$parts = explode('\\', $clean_fqcn);
$simple_name = end($parts);
// Check if this FQCN actually exists in the manifest
// This prevents false positives for non-existent classes
$class_exists = false;
try {
// Try to find the class by simple name
$class_metadata = Manifest::php_get_metadata_by_class($simple_name);
// Verify the FQCN matches
if ($class_metadata && isset($class_metadata['fqcn'])) {
$manifest_fqcn = $class_metadata['fqcn'];
// Compare without leading backslash
if (ltrim($manifest_fqcn, '\\') === $clean_fqcn) {
$class_exists = true;
}
}
} catch (\RuntimeException $e) {
// Class not found in manifest - not a real class reference
continue;
}
// Only report violation if the class actually exists
if (!$class_exists) {
continue;
}
// Calculate line number from offset
$line = 1;
for ($i = 0; $i < $offset; $i++) {
if ($contents[$i] === "\n") {
$line++;
}
}
// Extract the line containing the violation for context
$lines = explode("\n", $contents);
$code_snippet = '';
if ($line > 0 && $line <= count($lines)) {
$code_snippet = trim($lines[$line - 1]);
}
$message = "Direct FQCN reference '{$fqcn}' in Blade template is not allowed. RSX classes are path-agnostic and should be referenced by simple name only.";
$suggestion = "Replace '{$fqcn}' with '{$simple_name}'. The autoloader will automatically resolve the class.";
$this->add_violation(
$file_path,
$line,
$message,
$code_snippet,
$suggestion,
'high'
);
}
}
}

View File

@@ -0,0 +1,245 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Blade;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class InlineScript_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'BLADE-SCRIPT-01';
}
public function get_name(): string
{
return 'Blade Inline Script Check';
}
public function get_description(): string
{
return 'Enforces no inline JavaScript in blade views - all JavaScript must be in separate ES6 class files';
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* This rule should run during manifest scan to provide immediate feedback
*
* 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.
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* inline scripts in Blade files violate critical framework architecture patterns.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Process file during manifest update to extract inline script violations
*/
public function on_manifest_file_update(string $file_path, string $contents, array $metadata = []): ?array
{
// Skip layouts (they can have script tags for loading external scripts)
if (str_contains($file_path, 'layout') || str_contains($file_path, 'Layout')) {
return null;
}
$lines = explode("\n", $contents);
$violations = [];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for <script> tags
if (preg_match('/<script\b[^>]*>(?!.*src=)/i', $line)) {
$violations[] = [
'type' => 'inline_script',
'line' => $line_number,
'code' => trim($line)
];
break; // Only need to find first violation
}
// Check for jQuery ready patterns
if (preg_match('/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line) ||
preg_match('/\$\s*\(\s*function\s*\(/', $line) ||
preg_match('/jQuery\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line)) {
$violations[] = [
'type' => 'jquery_ready',
'line' => $line_number,
'code' => trim($line)
];
break; // Only need to find first violation
}
}
if (!empty($violations)) {
return ['inline_script_violations' => $violations];
}
return null;
}
/**
* Check blade file for inline script violations stored in metadata
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip layouts
if (str_contains($file_path, 'layout') || str_contains($file_path, 'Layout')) {
return;
}
// Check for violations in code quality metadata
if (isset($metadata['code_quality_metadata']['BLADE-SCRIPT-01']['inline_script_violations'])) {
$violations = $metadata['code_quality_metadata']['BLADE-SCRIPT-01']['inline_script_violations'];
// Throw on first violation
foreach ($violations as $violation) {
$rsx_id = $this->extract_rsx_id($contents);
if ($violation['type'] === 'inline_script') {
$error_message = "Code Quality Violation (BLADE-SCRIPT-01) - Inline Script in Blade View\n\n";
$error_message .= "CRITICAL: Inline <script> tags are not allowed in blade views\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$violation['line']}\n";
$error_message .= "Code: {$violation['code']}\n\n";
$error_message .= $this->get_detailed_remediation($file_path, $rsx_id);
} else {
$error_message = "Code Quality Violation (BLADE-SCRIPT-01) - jQuery Ready Pattern in Blade View\n\n";
$error_message .= "CRITICAL: jQuery ready patterns are not allowed - use ES6 class with on_app_ready()\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$violation['line']}\n";
$error_message .= "Code: {$violation['code']}\n\n";
$error_message .= $this->get_detailed_remediation($file_path, $rsx_id);
}
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($file_path),
$violation['line']
);
}
}
}
/**
* Extract RSX ID from blade file content
*/
private function extract_rsx_id(string $contents): ?string
{
if (preg_match('/@rsx_id\s*\(\s*[\'"]([^\'"]+)[\'"]/', $contents, $matches)) {
return $matches[1];
}
return null;
}
/**
* Get detailed remediation instructions
*/
private function get_detailed_remediation(string $file_path, ?string $rsx_id): string
{
// Determine the JS filename and class name
$path_parts = pathinfo($file_path);
$blade_name = str_replace('.blade', '', $path_parts['filename']);
// If we have an RSX ID, use it as the class name
if ($rsx_id) {
$class_name = str_replace('.', '_', $rsx_id);
$js_filename = strtolower(str_replace('_', '_', $class_name)) . '.js';
} else {
// Fallback to blade filename
$class_name = ucfirst($blade_name);
$js_filename = $blade_name . '.js';
}
$js_path = dirname($file_path) . '/' . $js_filename;
return "FRAMEWORK CONVENTION: JavaScript for blade views must be in separate ES6 class files.
REQUIRED STEPS:
1. Create a JavaScript file: {$js_path}
2. Name the ES6 class exactly: {$class_name}
3. Use the on_app_ready() lifecycle method for initialization
4. Check for the view's presence using the RSX ID class selector
EXAMPLE IMPLEMENTATION for {$js_filename}:
/**
* JavaScript for {$class_name} view
*/
class {$class_name} {
/**
* Initialize when app is ready
* This method is automatically called by RSX framework
* No manual registration is required
*/
static on_app_ready() {
// CRITICAL: Only initialize if we're on this specific view
// The RSX framework adds the RSX ID as a class to the body tag
if (!\$('.{$class_name}').exists()) {
return;
}
console.log('{$class_name} view initialized');
// Add your view-specific JavaScript here
// Example: bind events, initialize components, etc.
// If you need to bind events to dynamically loaded components:
\$('#load-component-btn').on('click', function() {
\$('#dynamic-component-target').component('User_Card', {
data: {
name: 'Dynamic User',
email: 'dynamic@example.com'
}
});
});
}
}
KEY CONVENTIONS:
- Class name MUST match the RSX ID (with dots replaced by underscores)
- MUST use static on_app_ready() method (called automatically by framework)
- MUST check for view presence using \$('.{$class_name}').exists()
- MUST return early if view is not present (prevents code from running on wrong pages)
- NO manual registration needed - framework auto-discovers and calls on_app_ready()
- NO $(document).ready() or jQuery ready patterns allowed
- NO window.onload or DOMContentLoaded events allowed
WHY THIS MATTERS:
- Separation of concerns: HTML structure separate from behavior
- Framework integration: Automatic lifecycle management
- Performance: JavaScript only loads and runs where needed
- Maintainability: Clear file organization and naming conventions
- LLM-friendly: Predictable patterns that AI assistants can follow
The RSX framework will automatically:
1. Discover your ES6 class via the manifest
2. Call on_app_ready() after all components are initialized
3. Ensure proper execution order through lifecycle phases";
}
}

View File

@@ -0,0 +1,288 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Blade;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class UnbalancedTags_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'BLADE-TAGS-01';
}
public function get_name(): string
{
return 'Balanced HTML Tags in Control Flow';
}
public function get_description(): string
{
return 'Enforces that HTML tags opened within control flow blocks (@if/@foreach/etc) must be closed within the same block';
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
public function is_called_during_manifest_scan(): bool
{
return false;
}
/**
* Check blade file for unbalanced HTML tags in control flow blocks
*
* Simpler rule: If a block's first non-whitespace content is an opening HTML tag,
* that tag must be closed before the block ends.
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Parse the blade file into control flow blocks
$blocks = $this->parse_control_flow_blocks($contents);
foreach ($blocks as $block) {
$violation = $this->check_first_tag_closed($block['content'], $block['type']);
if ($violation !== null) {
$error_message = "Code Quality Violation (BLADE-TAGS-01) - Unbalanced HTML Tags in Control Flow\n\n";
$error_message .= "CRITICAL: Opening tag in control flow block must be closed within the same block\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Block: {$block['type']} at line {$block['start_line']}\n";
$error_message .= "First tag: <{$violation['tag']}> at line {$violation['opening_line']}\n";
$error_message .= "Problem: {$violation['message']}\n\n";
$error_message .= $this->get_remediation($block['type']);
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($file_path),
$block['start_line'] + $violation['opening_line'] - 1
);
}
}
}
/**
* Parse control flow blocks from blade content
*/
private function parse_control_flow_blocks(string $contents): array
{
$lines = explode("\n", $contents);
$blocks = [];
$stack = [];
// Control flow patterns
$start_patterns = [
'@if' => '@endif',
'@elseif' => '@endif',
'@else' => '@endif',
'@foreach' => '@endforeach',
'@for' => '@endfor',
'@while' => '@endwhile',
'@unless' => '@endunless',
'<?php if' => '<?php endif',
'<?php foreach' => '<?php endforeach',
'<?php for' => '<?php endfor',
'<?php while' => '<?php endwhile',
];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for block start
foreach ($start_patterns as $start => $end) {
if (preg_match('/^\s*' . preg_quote($start, '/') . '\b/', $line)) {
// Special handling for @else and @elseif - close previous block first
if (in_array($start, ['@elseif', '@else']) && !empty($stack)) {
$prev_block = array_pop($stack);
$prev_block['end_line'] = $line_number - 1;
$blocks[] = $prev_block;
}
$stack[] = [
'type' => $start,
'start_line' => $line_number,
'content' => '',
'lines' => [],
];
continue 2;
}
}
// Check for block end
foreach ($start_patterns as $start => $end) {
if (preg_match('/^\s*' . preg_quote($end, '/') . '\b/', $line)) {
if (!empty($stack)) {
$block = array_pop($stack);
$block['end_line'] = $line_number;
$blocks[] = $block;
}
continue 2;
}
}
// Add line to current block
if (!empty($stack)) {
$stack[count($stack) - 1]['content'] .= $line . "\n";
$stack[count($stack) - 1]['lines'][$line_number] = $line;
}
}
return $blocks;
}
/**
* Check if the first HTML tag in a block is closed within that block
* Returns null if OK, or array with violation details if not
*/
private function check_first_tag_closed(string $content, string $block_type): ?array
{
$lines = explode("\n", $content);
// Void elements that don't need closing tags
$void_elements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'link', 'meta', 'param', 'source', 'track', 'wbr'];
// Find the first non-whitespace line with an opening HTML tag
$first_tag = null;
$first_tag_line = null;
foreach ($lines as $line_num => $line) {
$trimmed = trim($line);
// Skip empty lines and blade comments
if (empty($trimmed) || str_starts_with($trimmed, '{{--')) {
continue;
}
// Look for opening HTML tag at start of line (after whitespace)
if (preg_match('/^<([a-zA-Z][a-zA-Z0-9_-]*)[^>]*>/i', $trimmed, $match)) {
$tag_name = strtolower($match[1]);
// Skip void elements
if (in_array($tag_name, $void_elements)) {
continue;
}
// Skip self-closing tags
if (str_ends_with(trim($match[0]), '/>')) {
continue;
}
// Found first opening tag
$first_tag = $tag_name;
$first_tag_line = $line_num + 1;
break;
}
// If we hit non-tag content first, no violation possible
if (!empty($trimmed)) {
return null;
}
}
// If no opening tag found, nothing to check
if ($first_tag === null) {
return null;
}
// Now check if the closing tag appears in the block
$closing_tag = "</{$first_tag}>";
foreach ($lines as $line) {
if (stripos($line, $closing_tag) !== false) {
// Found the closing tag - all good
return null;
}
}
// Closing tag not found in block
return [
'tag' => $first_tag,
'opening_line' => $first_tag_line,
'message' => "Closing tag </{$first_tag}> not found before end of {$block_type} block",
];
}
/**
* Get detailed remediation instructions
*/
private function get_remediation(string $block_type): string
{
return "FRAMEWORK CONVENTION: HTML tags must be balanced within control flow blocks.
This antipattern is commonly called \"Split Tag Conditionals\" or \"Broken HTML Nesting\".
PROBLEM: Control flow statements split what should be a single lexical unit (a complete HTML element).
WHY THIS IS FORBIDDEN:
1. Parser confusion - Syntax highlighters and formatters can't parse it correctly
2. Maintainability nightmare - Hard to see complete element structure
3. Error-prone - Easy to create invalid HTML when modifying
4. Mental overhead - Reader must mentally reconstruct tags across control flow
CORRECT ALTERNATIVES:
Option 1: Inline ternary (best for simple cases)
────────────────────────────────────────────────
@if(\$active)
<div class=\"card active\">Content</div>
@else
<div class=\"card inactive\">Content</div>
@endif
Option 2: Pre-compute classes (best for complex attributes)
────────────────────────────────────────────────────────────
@php
\$card_class = \$active ? 'active' : 'inactive';
@endphp
<div class=\"card {{ \$card_class }}\">
Content
</div>
Option 3: Duplicate complete tags (best when tags differ significantly)
────────────────────────────────────────────────────────────────────────
@if(\$show_form)
<form action=\"/submit\" method=\"POST\">
<input type=\"text\" name=\"value\">
<button>Submit</button>
</form>
@else
<div class=\"info-message\">
Form is disabled
</div>
@endif
NEVER DO THIS:
────────────────
@if(\$required)
<select required>
@else
<select>
@endif
<option>Value</option>
</select>
CORRECT:
───────────
@if(\$required)
<select required>
<option>Value</option>
</select>
@else
<select>
<option>Value</option>
</select>
@endif
PRINCIPLE: Control flow should NEVER split lexical units. HTML elements are atomic structures - keep them whole.";
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace App\RSpade\CodeQuality\Rules;
use App\RSpade\CodeQuality\CodeQuality_Violation;
use App\RSpade\CodeQuality\Support\ViolationCollector;
#[Instantiatable]
abstract class CodeQualityRule_Abstract
{
protected ViolationCollector $collector;
protected array $config = [];
protected bool $enabled = true;
public function __construct(ViolationCollector $collector, array $config = [])
{
$this->collector = $collector;
$this->config = $config;
$this->enabled = $config['enabled'] ?? true;
}
/**
* Get the unique rule identifier (e.g., 'PHP-SC-001')
*/
abstract public function get_id(): string;
/**
* Get human-readable rule name
*/
abstract public function get_name(): string;
/**
* Get rule description
*/
abstract public function get_description(): string;
/**
* Get file patterns this rule applies to (e.g., ['*.php', '*.js'])
*/
abstract public function get_file_patterns(): array;
/**
* Check a file for violations
*
* @param string $file_path Absolute path to file
* @param string $contents File contents (may be sanitized)
* @param array $metadata Additional metadata from manifest
*/
abstract public function check(string $file_path, string $contents, array $metadata = []): void;
/**
* Whether this rule supports checking Console Commands
*
* Rules that return true here will be given Console Command files to check
* when using default paths or when a specific Console Command file is provided.
* Rules supporting Console Commands MUST NOT rely on manifest metadata as
* Console Commands are not indexed in the manifest.
*
* @return bool
*/
public function supports_console_commands(): bool
{
return false;
}
/**
* Check if this rule is enabled
*/
public function is_enabled(): bool
{
return $this->enabled;
}
/**
* Check if this rule should be 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.
*
* @return bool
*/
public function is_called_during_manifest_scan(): bool
{
return false;
}
/**
* Get default severity for this rule
*/
public function get_default_severity(): string
{
return 'medium';
}
/**
* Add a violation
*/
protected function add_violation(
string $file_path,
int $line_number,
string $message,
?string $code_snippet = null,
?string $suggestion = null,
?string $severity = null
): void {
$violation = new CodeQuality_Violation(
rule_id: $this->get_id(),
file_path: $file_path,
line_number: $line_number,
message: $message,
severity: $severity ?? $this->get_default_severity(),
code_snippet: $code_snippet,
suggestion: $suggestion
);
$this->collector->add($violation);
}
/**
* Extract code snippet around a line
*/
protected function get_code_snippet(array $lines, int $line_index, int $context = 2): string
{
$start = max(0, $line_index - $context);
$end = min(count($lines) - 1, $line_index + $context);
$snippet = [];
for ($i = $start; $i <= $end; $i++) {
$prefix = $i === $line_index ? '>>> ' : ' ';
$snippet[] = $prefix . ($i + 1) . ': ' . $lines[$i];
}
return implode("\n", $snippet);
}
/**
* Get all PHP files in the Console Commands directory
*
* This helper allows rules to optionally include Console Commands in their checks
* without requiring these files to be in the manifest. Rules using this helper
* MUST NOT rely on manifest metadata since Console Commands are not indexed.
*
* @return array Array of absolute file paths to PHP files in app/Console/Commands
*/
protected static function get_console_command_files(): array
{
$commands_dir = base_path('app/Console/Commands');
if (!is_dir($commands_dir)) {
return [];
}
$files = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($commands_dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}
return $files;
}
/**
* Check if a file is a Console Command
*
* @param string $file_path The file path to check
* @return bool True if the file is in app/Console/Commands
*/
protected static function is_console_command(string $file_path): bool
{
$commands_dir = base_path('app/Console/Commands');
$normalized_path = str_replace('\\', '/', $file_path);
$normalized_commands_dir = str_replace('\\', '/', $commands_dir);
return str_starts_with($normalized_path, $normalized_commands_dir);
}
}

View File

@@ -0,0 +1,245 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class AbstractClassNaming_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'ABSTRACT-CLASS-01';
}
public function get_name(): string
{
return 'Abstract Class Naming Convention';
}
public function get_description(): string
{
return 'Ensures abstract classes follow RSX naming conventions with _Abstract suffix';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js'];
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip -temp files
if (str_contains($file_path, '-temp.')) {
return;
}
// Only check files in ./rsx and ./app/RSpade
$is_rsx = str_contains($file_path, '/rsx/');
$is_rspade = str_contains($file_path, '/app/RSpade/');
if (!$is_rsx && !$is_rspade) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if ($is_rspade && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$filename = basename($file_path);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Handle PHP files
if ($extension === 'php') {
$this->check_php_abstract_class($file_path, $contents, $metadata);
}
// Handle JavaScript files
elseif ($extension === 'js') {
$this->check_js_abstract_class($file_path, $contents, $metadata);
}
}
private function check_php_abstract_class(string $file_path, string $contents, array $metadata): void
{
// Check if this is an abstract class
if (!preg_match('/^\s*abstract\s+class\s+(\w+)/m', $contents, $matches)) {
return; // Not an abstract class
}
$class_name = $matches[1];
$filename = basename($file_path);
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
// Check class name ends with _Abstract
if (!str_ends_with($class_name, '_Abstract')) {
$this->add_violation(
$file_path,
0,
"Abstract class '$class_name' must end with '_Abstract'",
"abstract class $class_name",
$this->get_abstract_class_remediation($class_name, $filename, true),
'high'
);
return; // Don't check filename if class name is wrong
}
// Check filename ends with _abstract.php or Abstract.php
if (!str_ends_with($filename, '_abstract.php') && !str_ends_with($filename, 'Abstract.php')) {
$this->add_violation(
$file_path,
0,
"Abstract class file '$filename' must end with '_abstract.php' or 'Abstract.php'",
"File: $filename",
$this->get_abstract_filename_remediation($class_name, $filename),
'medium'
);
}
}
private function check_js_abstract_class(string $file_path, string $contents, array $metadata): void
{
// Check for classes ending with _Abstract
if (!isset($metadata['class'])) {
return;
}
$class_name = $metadata['class'];
// Only check classes that end with _Abstract
if (!str_ends_with($class_name, '_Abstract')) {
return;
}
$filename = basename($file_path);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Check filename ends with _abstract.js or Abstract.js
$valid_endings = ["_abstract.$extension", "Abstract.$extension"];
$valid = false;
foreach ($valid_endings as $ending) {
if (str_ends_with($filename, $ending)) {
$valid = true;
break;
}
}
if (!$valid) {
$this->add_violation(
$file_path,
0,
"Abstract class file '$filename' must end with '_abstract.$extension' or 'Abstract.$extension'",
"File: $filename",
$this->get_abstract_filename_remediation($class_name, $filename),
'medium'
);
}
}
private function get_abstract_class_remediation(string $class_name, string $filename, bool $is_php): string
{
// Determine suggested class name based on current naming
$suggested_class = $this->suggest_abstract_class_name($class_name);
$is_rsx = str_contains($filename, '_');
return "ABSTRACT CLASS NAMING CONVENTION
Abstract classes must follow RSX naming patterns:
- Class name must end with '_Abstract'
- Filename must end with '_abstract.php' or 'Abstract.php'
CURRENT CLASS: $class_name
SUGGESTED CLASS: $suggested_class
SUGGESTED FIXES:
1. Rename class from '$class_name' to '$suggested_class'
2. Rename file to match:
- For /rsx: Use lowercase convention (e.g., " . strtolower(str_replace('_', '_', $suggested_class)) . ".php)
- For /app/RSpade: Use exact match (e.g., $suggested_class.php)
3. Update all references to this class
WHY THIS MATTERS:
- Makes abstract classes immediately identifiable
- Enables framework introspection and auto-discovery
- Maintains consistent naming patterns across the codebase";
}
private function get_abstract_filename_remediation(string $class_name, string $filename): string
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$lowercase_suggestion = strtolower(str_replace('_', '_', $class_name)) . ".$extension";
$exact_suggestion = $class_name . ".$extension";
return "ABSTRACT CLASS FILENAME CONVENTION
Abstract class files must end with '_abstract.$extension' or 'Abstract.$extension'
CURRENT FILE: $filename
CLASS NAME: $class_name
VALID FILENAME PATTERNS:
- Lowercase with underscore: *_abstract.$extension
- Uppercase suffix: *Abstract.$extension
SUGGESTED FILENAMES:
- For /rsx: $lowercase_suggestion
- For /app/RSpade: $exact_suggestion
Note: Both patterns are valid in either directory.";
}
private function suggest_abstract_class_name(string $current_name): string
{
// If class name contains 'Abstract' but not at the end
if (stripos($current_name, 'Abstract') !== false && !str_ends_with($current_name, '_Abstract') && !str_ends_with($current_name, 'Abstract')) {
// Remove 'Abstract' from wherever it is
$without_abstract = preg_replace('/Abstract/i', '', $current_name);
$without_abstract = trim($without_abstract, '_');
// If the class has underscores, add _Abstract
if (str_contains($without_abstract, '_')) {
return $without_abstract . '_Abstract';
} else {
// For non-underscore classes, add Abstract at the end
return $without_abstract . 'Abstract';
}
}
// If class doesn't contain 'Abstract' at all
if (str_contains($current_name, '_')) {
return $current_name . '_Abstract';
} else {
return $current_name . 'Abstract';
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class Assignment_Comparison_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'COMMON-ASSIGN-01';
}
public function get_name(): string
{
return 'Assignment vs Comparison Check';
}
public function get_description(): string
{
return 'Detects assignment operator (=) used where comparison (== or ===) expected';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js', '*.jsx', '*.ts', '*.tsx'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check if file is in allowed directories using same logic as rsx:check command
*/
private function is_file_in_allowed_directories(string $file_path): bool
{
// Get scan directories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
$relative_path = str_replace(base_path() . '/', '', $file_path);
// Special case: Allow Console Command files
if (str_starts_with($relative_path, 'app/Console/Commands/')) {
return true;
}
// Check against configured scan directories
foreach ($scan_directories as $scan_path) {
// Skip specific file entries in scan_directories
if (str_contains($scan_path, '.')) {
// This is a specific file, check exact match
if ($relative_path === $scan_path) {
return true;
}
} else {
// This is a directory, check if file is within it
if (str_starts_with($relative_path, rtrim($scan_path, '/') . '/') ||
rtrim($relative_path, '/') === rtrim($scan_path, '/')) {
return true;
}
}
}
return false;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Use the same directory filtering logic as rsx:check command
if (!$this->is_file_in_allowed_directories($file_path)) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Determine file type
$is_php = str_ends_with($file_path, '.php');
$is_js = str_ends_with($file_path, '.js') || str_ends_with($file_path, '.jsx') ||
str_ends_with($file_path, '.ts') || str_ends_with($file_path, '.tsx');
if (!$is_php && !$is_js) {
return;
}
// Use original file content directly (no sanitization)
$original_content = file_get_contents($file_path);
$lines = explode("\n", $original_content);
// Process each line individually
// Note: We're letting multi-line conditions slide for simplicity - this catches 99% of violations
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip empty lines
if (trim($line) === '') {
continue;
}
// Skip lines that are just comments (start with //)
$trimmed = trim($line);
if (str_starts_with($trimmed, '//')) {
continue;
}
// Check for single = in if statement condition (must have complete condition on same line)
if (preg_match('/\bif\s*\(([^)]+)\)/', $line, $match)) {
$condition = $match[1];
// Skip if there's a // comment before the if statement
if (preg_match('/\/\/.*\bif\s*\(/', $line)) {
continue;
}
// Remove quoted strings to avoid false positives from regex patterns
// This prevents flagging preg_match('/pattern=value/', $var)
$condition_no_quotes = preg_replace('/([\'"])[^\\1]*?\\1/', '""', $condition);
// Check for single = that's not part of ==, ===, !=, !==, <=, >=
// Must have non-equals char before and after the single =
if (preg_match('/[^=!<>]\s*=\s*[^=]/', $condition_no_quotes)) {
// Double-check it's not a comparison operator
if (!preg_match('/(?:==|===|!=|!==|<=|>=)/', $condition_no_quotes)) {
$this->add_violation(
$file_path,
$line_number,
"Assignment operator (=) used in if statement where comparison expected.",
trim($line),
"Assignment and truthiness checks must be on separate lines. " .
"The pattern 'if (\$var = function())' is not acceptable in RSpade code. " .
"Split into two lines: '\$var = function(); if (\$var) { ... }'. " .
"If you meant comparison, use == or === instead of =. " .
"This rule enforces code clarity by separating assignment from condition evaluation.",
'high'
);
}
}
}
// Skip while statements - assignment in while is acceptable
// The pattern while ($var = function()) is allowed for iteration
// Check for single = in for loop condition (middle part)
if (preg_match('/\bfor\s*\(([^;]*);([^;]*);([^)]*)\)/', $line, $match)) {
$condition = $match[2]; // The middle part is the condition
// Skip if there's a // comment before the for statement
if (preg_match('/\/\/.*\bfor\s*\(/', $line)) {
continue;
}
// Remove quoted strings to avoid false positives
$condition_no_quotes = preg_replace('/([\'"])[^\\1]*?\\1/', '""', $condition);
if (trim($condition_no_quotes) && preg_match('/[^=!<>]\s*=\s*[^=]/', $condition_no_quotes)) {
if (!preg_match('/(?:==|===|!=|!==|<=|>=)/', $condition_no_quotes)) {
$this->add_violation(
$file_path,
$line_number,
"Assignment operator (=) used in for loop condition where comparison expected.",
trim($line),
"Use == or === for comparison in the for loop condition (second part). " .
"Assignment in the condition will always evaluate to the assigned value, not perform a comparison. " .
"Example: change 'for(i=0; i=5; i++)' to 'for(i=0; i==5; i++)'.",
'high'
);
}
}
}
// Skip do...while statements - assignment in while is acceptable
// The pattern } while ($var = function()) is allowed for iteration
// PHP-specific: Check for single = in elseif statement
if ($is_php && preg_match('/\belseif\s*\(([^)]+)\)/', $line, $match)) {
$condition = $match[1];
// Skip if there's a // comment before the elseif
if (preg_match('/\/\/.*\belseif\s*\(/', $line)) {
continue;
}
// Remove quoted strings to avoid false positives
$condition_no_quotes = preg_replace('/([\'"])[^\\1]*?\\1/', '""', $condition);
if (preg_match('/[^=!<>]\s*=\s*[^=]/', $condition_no_quotes)) {
if (!preg_match('/(?:==|===|!=|!==|<=|>=)/', $condition_no_quotes)) {
$this->add_violation(
$file_path,
$line_number,
"Assignment operator (=) used in elseif statement where comparison expected.",
trim($line),
"Use == or === for comparison in elseif statements. " .
"Assignment in an elseif condition will execute the assignment and evaluate the assigned value, not perform a comparison. " .
"Example: change 'elseif(x = 5)' to 'elseif(x == 5)' or 'elseif(x === 5)'.",
'high'
);
}
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class CommandOrganization_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CMD-ORG-01';
}
public function get_name(): string
{
return 'Command Organization Check';
}
public function get_description(): string
{
return 'Ensures commands are organized in proper subdirectories based on signature';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Special method for checking commands organization
* Only runs when using default paths
*/
public function check_commands(): void
{
// Only run when using default paths
if (!($this->config['using_default_paths'] ?? false)) {
return;
}
$commands_dir = base_path('app/Console/Commands');
if (!is_dir($commands_dir)) {
return;
}
// Get files in root Commands directory only (not subdirectories)
$files = glob($commands_dir . '/*.php');
foreach ($files as $file) {
$filename = basename($file);
$content = file_get_contents($file);
// Check for rsx: commands
if (preg_match('/\$signature\s*=\s*[\'"]rsx:/i', $content)) {
$this->add_violation(
$file,
0,
"RSX command found in root Commands directory",
"File: $filename contains 'rsx:' command signature",
"COMMAND ORGANIZATION VIOLATION\n\n" .
"This command has signature starting with 'rsx:' but is not in the Rsx subdirectory.\n\n" .
"REQUIRED ACTION:\n" .
"Move this file to app/RSpade/Commands/Rsx/ or one of its subdirectories.\n\n" .
"WHY THIS MATTERS:\n" .
"- All RSX framework commands must be organized in the Rsx subdirectory\n" .
"- This keeps framework commands separate from application commands\n" .
"- Maintains consistent command organization\n" .
"- Makes it easier to find related commands\n\n" .
"STEPS TO FIX:\n" .
"1. Move the file: mv $file " . base_path('app/RSpade/Commands/Rsx/') . "\n" .
"2. Update the namespace to include \\Rsx\n" .
"3. Run 'composer dump-autoload' to update class mappings",
'high'
);
}
// Check for migrate: commands
if (preg_match('/\$signature\s*=\s*[\'"]migrate:/i', $content)) {
$this->add_violation(
$file,
0,
"Migration command found in root Commands directory",
"File: $filename contains 'migrate:' command signature",
"COMMAND ORGANIZATION VIOLATION\n\n" .
"This command has signature starting with 'migrate:' but is not in the Migrate subdirectory.\n\n" .
"REQUIRED ACTION:\n" .
"Move this file to app/Console/Commands/Migrate/ or one of its subdirectories.\n\n" .
"WHY THIS MATTERS:\n" .
"- All migration-related commands must be organized in the Migrate subdirectory\n" .
"- This groups database migration tools together\n" .
"- Maintains consistent command organization\n" .
"- Makes it easier to find migration-related commands\n\n" .
"STEPS TO FIX:\n" .
"1. Move the file: mv $file " . $commands_dir . "/Migrate/\n" .
"2. Update the namespace to include \\Migrate\n" .
"3. Run 'composer dump-autoload' to update class mappings",
'high'
);
}
}
}
/**
* Standard check method - not used for this rule
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// This rule uses check_commands instead
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\Core\Manifest\Manifest;
class DuplicateCaseFiles_CodeQualityRule extends CodeQualityRule_Abstract
{
private static bool $checked = false;
public function get_id(): string
{
return 'FILE-CASE-DUP-01';
}
public function get_name(): string
{
return 'Duplicate Files with Different Case';
}
public function get_description(): string
{
return 'Detects files with same name but different case - breaks Windows/macOS compatibility';
}
public function get_file_patterns(): array
{
return ['*']; // Check all files
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Whether this rule is called during manifest scan
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* duplicate case files break Windows/macOS compatibility and must be caught immediately.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Check for duplicate files with different case
* This checks the entire manifest once rather than per-file
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only run this check once for the entire manifest
if (self::$checked) {
return;
}
self::$checked = true;
// Get all files from the manifest
$all_files = Manifest::get_all();
// Build map of directories to files
$files_by_dir = [];
foreach ($all_files as $file => $file_metadata) {
// Skip vendor and node_modules
if (str_contains($file, '/vendor/') || str_contains($file, '/node_modules/')) {
continue;
}
$dir = dirname($file);
$filename = basename($file);
if (!isset($files_by_dir[$dir])) {
$files_by_dir[$dir] = [];
}
$files_by_dir[$dir][] = $filename;
}
// Check each directory for case-insensitive duplicates
foreach ($files_by_dir as $dir => $filenames) {
$seen_lowercase = [];
foreach ($filenames as $filename) {
$filename_lower = strtolower($filename);
if (isset($seen_lowercase[$filename_lower])) {
$existing = $seen_lowercase[$filename_lower];
// Only report if actual case is different
if ($existing !== $filename) {
$file1 = $dir . '/' . $existing;
$file2 = $dir . '/' . $filename;
// Count uppercase characters to determine which file to favor
$uppercase_count1 = preg_match_all('/[A-Z]/', $existing);
$uppercase_count2 = preg_match_all('/[A-Z]/', $filename);
// Favor the file with more uppercase characters
$preferred_file = ($uppercase_count2 > $uppercase_count1) ? $file2 : $file1;
$error_message = "Code Quality Violation (FILE-CASE-DUP-01) - Duplicate files with different case\n\n";
$error_message .= "CRITICAL: This BREAKS Windows/macOS compatibility!\n\n";
$error_message .= "Directory: {$dir}\n";
$error_message .= "File 1: {$existing}\n";
$error_message .= "File 2: {$filename}\n\n";
$error_message .= "Resolution:\n";
$error_message .= "1. Run: diff -u '{$file1}' '{$file2}' to compare\n";
$error_message .= "2. Determine which file has the correct functionality\n";
$error_message .= "3. Remove the incorrect/older file\n";
$error_message .= "4. Update all references to use the correct filename\n";
$error_message .= "5. Test thoroughly - IDE may have been using wrong file!";
// Throw immediately on first duplicate found
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($preferred_file),
1
);
}
} else {
$seen_lowercase[$filename_lower] = $filename;
}
}
}
// Reset for next manifest build
self::$checked = false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class FilenameCase_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-CASE-01';
}
public function get_name(): string
{
return 'Filename Case Check';
}
public function get_description(): string
{
return 'All files in rsx/ should be lowercase';
}
public function get_file_patterns(): array
{
return ['*.*']; // All files
}
public function get_default_severity(): string
{
return 'low';
}
/**
* Check if a filename contains uppercase characters (for RSX files) - from line 1706
* Excludes vendor and resource directories, and .md files
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and resource directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/resource/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ directory
if (!str_contains($file_path, '/rsx/')) {
return;
}
// Get just the filename without the directory path
$filename = basename($file_path);
// Skip .md files
if (str_ends_with($filename, '.md')) {
return;
}
// Check if filename contains uppercase characters
if (preg_match('/[A-Z]/', $filename)) {
// Convert to lowercase for suggestion
$suggested = strtolower($filename);
$this->add_violation(
$file_path,
0,
"Filename '{$filename}' contains uppercase characters. All files in rsx/ should be lowercase.",
$filename,
"Rename file to '{$suggested}'. Remember: class names should still use First_Letter_Uppercase format.",
'low'
);
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class FilenameEnhanced_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-ENHANCED-01';
}
public function get_name(): string
{
return 'Enhanced Filename Check';
}
public function get_description(): string
{
return "Detects 'enhanced' in filenames which indicates parallel implementation - a critical code smell";
}
public function get_file_patterns(): array
{
return ['*.*']; // All files
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check if a filename contains 'enhanced' which indicates parallel implementation (from line 1741)
* This is a critical code smell indicating technical debt
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get just the filename without the directory path
$filename = basename($file_path);
// Check if filename contains 'enhanced' (case insensitive)
if (stripos($filename, 'enhanced') !== false) {
// Check if file has exemption marker
$content = file_get_contents($file_path);
if (str_contains($content, '//@enhanced_filename_allowed')) {
return; // File is explicitly exempted
}
// Extract the base name without 'enhanced' for suggestion
$suggested = preg_replace('/[_\-]?enhanced[_\-]?/i', '', $filename);
$resolution = "CRITICAL: Filename contains 'enhanced' which indicates parallel implementation of existing functionality.\n\n";
$resolution .= "INVESTIGATION REQUIRED - Follow this procedure thoroughly (may take several hours):\n\n";
$resolution .= "1. IDENTIFY THE RELATIONSHIP:\n";
$resolution .= " - Search for similarly named files without 'enhanced' (e.g., if this is 'UserEnhanced.php', look for 'User.php')\n";
$resolution .= " - Use grep/search to find: '{$suggested}' and variations\n";
$resolution .= " - Check git history to understand when and why the 'enhanced' version was created\n\n";
$resolution .= "2. ANALYZE THE INVOCATION:\n";
$resolution .= " - Search the codebase for references to '{$filename}' to see how it's used\n";
$resolution .= " - Look for conditional logic, switches, or configuration that chooses between versions\n";
$resolution .= " - Identify any fallback patterns where one version is tried before another\n\n";
$resolution .= "3. COMPARE FUNCTIONALITY:\n";
$resolution .= " - Diff the enhanced file against the original (if found)\n";
$resolution .= " - Identify what improvements were made in the enhanced version\n";
$resolution .= " - Check if the original has any functionality missing from enhanced\n";
$resolution .= " - Document the differences and determine completeness of enhanced version\n\n";
$resolution .= "4. DETERMINE MIGRATION PATH:\n";
$resolution .= " If enhanced version is a complete replacement:\n";
$resolution .= " - Verify enhanced version has ALL necessary functionality from original\n";
$resolution .= " - Remove the old/original file\n";
$resolution .= " - Rename enhanced file to the original name (e.g., 'UserEnhanced.php' → 'User.php')\n";
$resolution .= " - Update all references to use the single, renamed file\n";
$resolution .= " - Remove any conditional/switching/fallback code\n\n";
$resolution .= " If versions serve different purposes:\n";
$resolution .= " - Rename the enhanced file to better describe its specific purpose\n";
$resolution .= " - Update documentation to clarify the distinct roles\n\n";
$resolution .= "5. IF UNCLEAR:\n";
$resolution .= " Present findings to the user including:\n";
$resolution .= " - List of similar files found\n";
$resolution .= " - How each version is invoked\n";
$resolution .= " - Key differences between versions\n";
$resolution .= " - Request guidance on consolidation strategy\n\n";
$resolution .= "IMPORTANT: The goal is to eliminate dual implementations. Having both 'document_parser.php' and 'enhanced_document_parser.php' creates:\n";
$resolution .= "- Confusion about which to use\n";
$resolution .= "- Maintenance burden keeping both in sync\n";
$resolution .= "- Potential bugs from inconsistent behavior\n";
$resolution .= "- Technical debt that compounds over time\n\n";
$resolution .= "To mark legitimate use (extremely rare): Add '//@enhanced_filename_allowed' comment to the file.\n";
$resolution .= "This should only be done when 'enhanced' is genuinely part of the domain language, not indicating an upgrade.";
$this->add_violation(
$file_path,
0,
"CRITICAL: Filename contains 'enhanced' which indicates parallel implementation of existing functionality.",
$filename,
$resolution,
'critical'
);
}
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class FilenameSpaces_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-SPACE-01';
}
public function get_name(): string
{
return 'Filename Spaces Check';
}
public function get_description(): string
{
return 'Filenames and directory paths must not contain spaces';
}
public function get_file_patterns(): array
{
// Return multiple common patterns to match all files
// The checker uses these with matches_pattern which does simple extension checking
return ['*.php', '*.js', '*.css', '*.scss', '*.blade.php', '*.json', '*.xml', '*.md', '*.txt', '*.yml', '*.yaml', '*.sql', '*.sh', '*.jqhtml', '*.ts', '*.tsx', '*.jsx', '*'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Whether this rule is called during manifest scan
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* spaces in filenames break shell commands and framework operations.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Process file during manifest update - throws immediately on violation
* This is more efficient than checking later
*/
public function on_manifest_file_update(string $file_path, string $contents, array $metadata = []): ?array
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return null;
}
// Get relative path from absolute
$relative_path = str_replace(base_path() . '/', '', $file_path);
// Check for spaces in the entire path
if (str_contains($relative_path, ' ')) {
// Get just the filename
$filename = basename($relative_path);
$dirname = dirname($relative_path);
// Build error message
if (str_contains($filename, ' ')) {
$suggested = str_replace(' ', '_', $filename);
$error_message = "Code Quality Violation (FILE-SPACE-01) - Filename '{$filename}' contains spaces\n\n";
$error_message .= "File: {$relative_path}\n\n";
$error_message .= "Spaces in filenames break shell commands, URLs, and tooling.\n\n";
$error_message .= "Resolution:\nRename file to '{$suggested}' (replace spaces with underscores or remove them).";
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
$file_path,
1
);
}
if (str_contains($dirname, ' ')) {
// Find which directory has the space
$path_parts = explode('/', $dirname);
$problematic_dirs = array_filter($path_parts, fn($part) => str_contains($part, ' '));
$problematic_str = implode(', ', $problematic_dirs);
$error_message = "Code Quality Violation (FILE-SPACE-01) - Directory path contains spaces\n\n";
$error_message .= "File: {$relative_path}\n\n";
$error_message .= "Directories with spaces: {$problematic_str}\n\n";
$error_message .= "Resolution:\nRename directories to remove spaces. This is critical as spaces in paths break shell commands, git operations, and various build tools.";
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
$file_path,
1
);
}
}
// No metadata needed - we throw on violation
return null;
}
/**
* Check if a filename or any directory in its path contains spaces
* This method is now just a fallback - on_manifest_file_update handles the real work
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Check for spaces in the entire path
if (str_contains($file_path, ' ')) {
// Get just the filename
$filename = basename($file_path);
$dirname = dirname($file_path);
// Determine if the space is in the filename or directory
if (str_contains($filename, ' ')) {
$suggested = str_replace(' ', '_', $filename);
$this->add_violation(
$file_path,
0,
"Filename '{$filename}' contains spaces which will cause issues with shell commands, URLs, and tooling.",
$filename,
"Rename file to '{$suggested}' (replace spaces with underscores or remove them).",
'critical'
);
}
if (str_contains($dirname, ' ')) {
// Find which directory has the space
$path_parts = explode('/', $dirname);
$problematic_dirs = array_filter($path_parts, fn($part) => str_contains($part, ' '));
$problematic_str = implode(', ', $problematic_dirs);
$this->add_violation(
$file_path,
0,
"Directory path contains spaces in: {$problematic_str}",
$dirname,
"Rename directories to remove spaces. This is critical as spaces in paths break shell commands, git operations, and various build tools.",
'critical'
);
}
}
}
}

View File

@@ -0,0 +1,358 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*
* @ROUTE-EXISTS-01-EXCEPTION - This file generates code templates with placeholder route names
*/
namespace App\RSpade\CodeQuality\Rules\Common;
use Illuminate\Support\Facades\Route;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\Core\Dispatch\Dispatcher;
use App\RSpade\Core\Manifest\Manifest;
/**
* HardcodedInternalUrlRule - Detect hardcoded internal URLs in href attributes
*
* This rule scans .blade.php and .jqhtml files for href attributes containing
* hardcoded internal routes (URLs starting with "/" without file extensions)
* and suggests using the proper route generation methods instead.
*/
class HardcodedInternalUrl_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique identifier for this rule
*
* @return string
*/
public function get_id(): string
{
return 'URL-HARDCODE-01';
}
/**
* Get the default severity level
*
* @return string One of: critical, high, medium, low, convention
*/
public function get_default_severity(): string
{
return 'medium';
}
/**
* Get the file patterns this rule applies to
*
* @return array
*/
public function get_file_patterns(): array
{
return ['*.blade.php', '*.jqhtml'];
}
/**
* Get the display name for this rule
*
* @return string
*/
public function get_name(): string
{
return 'Hardcoded Internal URL Detection';
}
/**
* Get the description of what this rule checks
*
* @return string
*/
public function get_description(): string
{
return 'Detects hardcoded internal URLs in href attributes and suggests using route generation methods';
}
/**
* Check the file contents for violations
*
* @param string $file_path The path to the file being checked
* @param string $contents The contents of the file
* @param array $metadata Additional metadata about the file
* @return void
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Initialize manifest to ensure routes are available
try {
Manifest::init();
} catch (\Exception $e) {
// If manifest fails to initialize, we can't check routes
return;
}
$is_jqhtml = str_ends_with($file_path, '.jqhtml');
$lines = explode("\n", $contents);
foreach ($lines as $line_num => $line) {
// Find all href attributes in the line
// Match href="..." or href='...'
if (preg_match_all('/href\s*=\s*["\']([^"\']+)["\']/', $line, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[1] as $match) {
$url = $match[0];
$position = $match[1];
// Check if this is a likely internal route
if (!$this->_is_likely_internal_route($url)) {
continue;
}
// Extract base URL and query params
$url_parts = parse_url($url);
$base_url = $url_parts['path'] ?? '/';
$query_string = $url_parts['query'] ?? '';
// Try to resolve the URL to a route
$route_info = null;
try {
$route_info = Dispatcher::resolve_url_to_route($base_url, 'GET');
} catch (\Exception $e) {
// URL doesn't resolve to a known route
continue;
}
$suggested_code = '';
if ($route_info) {
// Found RSX route
$controller_class = $route_info['class'] ?? '';
$method_name = $route_info['method'] ?? '';
$route_params = $route_info['params'] ?? [];
// Parse query string params
$query_params = [];
if ($query_string) {
parse_str($query_string, $query_params);
}
// Merge all params (route params take precedence)
$all_params = array_merge($query_params, $route_params);
// Extract just the class name without namespace
$class_parts = explode('\\', $controller_class);
$class_name = end($class_parts);
// Generate the suggested replacement code
$suggested_code = $this->_generate_suggested_code(
$class_name,
$method_name,
$all_params,
$is_jqhtml
);
} else {
// Check if it's a Laravel route
$laravel_route = $this->_find_laravel_route($base_url);
if ($laravel_route) {
$suggested_code = $this->_generate_laravel_suggestion($laravel_route, $query_string, $is_jqhtml);
} else {
// No route found, skip
continue;
}
}
// Add violation
$this->add_violation(
$line_num + 1,
$position,
"Hardcoded internal URL detected: {$url}",
$line,
"Use route generation instead:\n{$suggested_code}"
);
}
}
}
}
/**
* Check if a URL is likely an internal route
*
* @param string $url
* @return bool
*/
protected function _is_likely_internal_route(string $url): bool
{
// Must start with /
if (!str_starts_with($url, '/')) {
return false;
}
// Skip absolute URLs (with protocol)
if (preg_match('#^//#', $url)) {
return false;
}
// Extract path before query string
$path = strtok($url, '?');
// Get the last segment of the path
$segments = explode('/', trim($path, '/'));
$last_segment = end($segments);
// If last segment has a dot (file extension), it's likely a file not a route
if ($last_segment && str_contains($last_segment, '.')) {
return false;
}
// Skip common static asset paths
$static_prefixes = ['/assets/', '/css/', '/js/', '/images/', '/img/', '/fonts/', '/storage/'];
foreach ($static_prefixes as $prefix) {
if (str_starts_with($path, $prefix)) {
return false;
}
}
return true;
}
/**
* Generate suggested replacement code
*
* @param string $class_name
* @param string $method_name
* @param array $params
* @param bool $is_jqhtml
* @return string
*/
protected function _generate_suggested_code(string $class_name, string $method_name, array $params, bool $is_jqhtml): string
{
if ($is_jqhtml) {
// JavaScript version for .jqhtml files using <%= %> syntax
if (empty($params)) {
return "<%= Rsx.Route('{$class_name}', '{$method_name}').url() %>";
} else {
$params_json = json_encode($params, JSON_UNESCAPED_SLASHES);
return "<%= Rsx.Route('{$class_name}', '{$method_name}').url({$params_json}) %>";
}
} else {
// PHP version for .blade.php files
if (empty($params)) {
return "{{ Rsx::Route('{$class_name}', '{$method_name}')->url() }}";
} else {
$params_str = $this->_format_php_array($params);
return "{{ Rsx::Route('{$class_name}', '{$method_name}')->url({$params_str}) }}";
}
}
}
/**
* Format a PHP array for display
*
* @param array $params
* @return string
*/
protected function _format_php_array(array $params): string
{
$items = [];
foreach ($params as $key => $value) {
$key_str = var_export($key, true);
$value_str = var_export($value, true);
$items[] = "{$key_str} => {$value_str}";
}
return '[' . implode(', ', $items) . ']';
}
/**
* Find Laravel route by URL
*
* @param string $url
* @return string|null Route name if found
*/
protected function _find_laravel_route(string $url): ?string
{
// Get all Laravel routes
$routes = Route::getRoutes();
foreach ($routes as $route) {
// Check if URL matches this route's URI
if ($route->uri() === ltrim($url, '/')) {
// Get the route name if it has one
$name = $route->getName();
if ($name) {
return $name;
}
// No name, but route exists - return the URI for direct use
return $url;
}
}
return null;
}
/**
* Generate Laravel route suggestion
*
* @param string $route_name
* @param string $query_string
* @param bool $is_jqhtml
* @return string
*/
protected function _generate_laravel_suggestion(string $route_name, string $query_string, bool $is_jqhtml): string
{
// If route_name starts with /, it means no named route exists
if (str_starts_with($route_name, '/')) {
// Suggest adding a name to the route
$suggested_name = $this->_suggest_route_name($route_name);
if ($is_jqhtml) {
return "<%= '{$route_name}' %> <!-- Add name to route: ->name('{$suggested_name}'), then use route('{$suggested_name}') -->";
} else {
return "{{ route('{$suggested_name}') }}\n// First add ->name('{$suggested_name}') to the route definition in routes/web.php";
}
}
// Route has a name, use it
if ($is_jqhtml) {
// JavaScript version for .jqhtml files
if ($query_string) {
$query_params = [];
parse_str($query_string, $query_params);
$params_json = json_encode($query_params, JSON_UNESCAPED_SLASHES);
// Note: jqhtml would need a custom helper for Laravel routes
return "<%= route('{$route_name}', {$params_json}) %> <!-- Requires custom route() helper -->";
} else {
return "<%= route('{$route_name}') %> <!-- Requires custom route() helper -->";
}
} else {
// PHP version for .blade.php files
if ($query_string) {
$query_params = [];
parse_str($query_string, $query_params);
$params_str = $this->_format_php_array($query_params);
return "{{ route('{$route_name}', {$params_str}) }}";
} else {
return "{{ route('{$route_name}') }}";
}
}
}
/**
* Suggest a route name based on the URL path
*
* @param string $url
* @return string
*/
protected function _suggest_route_name(string $url): string
{
// Remove leading slash and convert to dot notation
$path = ltrim($url, '/');
// Convert path segments to route name
// /test-bundle-facade => test.bundle.facade
// /_idehelper => idehelper
$path = str_replace('_', '', $path); // Remove leading underscores
$path = str_replace('-', '.', $path); // Convert dashes to dots
$path = str_replace('/', '.', $path); // Convert slashes to dots
return $path ?: 'home';
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* Ensures SCSS manifest module has lower priority than modules it depends on
*
* The SCSS module needs to run after Blade, JavaScript, and Jqhtml modules
* because it checks their output to determine if SCSS files should have an ID
* based on matching class selectors.
*/
class ManifestModulePriority_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'MANIFEST-PRIORITY-01';
}
public function get_name(): string
{
return 'Manifest Module Priority Order';
}
public function get_description(): string
{
return 'Ensures SCSS manifest module runs after modules it depends on';
}
public function get_file_patterns(): array
{
// We'll check specific module files
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check the specific manifest module files
$module_files = [
'app/RSpade/Modules/Scss_ManifestModule.php',
'app/RSpade/Modules/Blade_ManifestModule.php',
'app/RSpade/Modules/JavaScript_ManifestModule.php',
'app/RSpade/Integrations/Jqhtml/Jqhtml_ManifestModule.php'
];
$is_module_file = false;
foreach ($module_files as $module_file) {
if (str_ends_with($file_path, $module_file)) {
$is_module_file = true;
break;
}
}
if (!$is_module_file) {
return;
}
// Extract priority from this file
$current_priority = $this->extract_priority($contents);
if ($current_priority === null) {
return; // Can't find priority method
}
// If this is the SCSS module, check all others
if (str_ends_with($file_path, 'Scss_ManifestModule.php')) {
$this->check_scss_priority($file_path, $current_priority);
}
}
/**
* Extract the priority value from module contents
*/
private function extract_priority(string $contents): ?int
{
// Look for: public function priority(): int { return NUMBER; }
if (preg_match('/public\s+function\s+priority\s*\(\s*\)\s*:\s*int\s*\{[^}]*return\s+(\d+)\s*;/s', $contents, $matches)) {
return (int)$matches[1];
}
return null;
}
/**
* Check that SCSS priority is lower (higher number) than all dependencies
*/
private function check_scss_priority(string $scss_file_path, int $scss_priority): void
{
$base_path = base_path();
$dependencies = [
'Blade_ManifestModule' => $base_path . '/app/RSpade/Modules/Blade_ManifestModule.php',
'JavaScript_ManifestModule' => $base_path . '/app/RSpade/Modules/JavaScript_ManifestModule.php',
'Jqhtml_ManifestModule' => $base_path . '/app/RSpade/Integrations/Jqhtml/Jqhtml_ManifestModule.php'
];
$violations = [];
foreach ($dependencies as $name => $path) {
if (!file_exists($path)) {
continue; // Module might not exist (e.g., Jqhtml is optional)
}
$contents = file_get_contents($path);
$priority = $this->extract_priority($contents);
if ($priority === null) {
continue; // Can't find priority
}
// SCSS priority should be higher number (lower priority) than dependencies
if ($scss_priority <= $priority) {
$violations[] = " - {$name}: priority={$priority} (SCSS has {$scss_priority})";
}
}
if (!empty($violations)) {
$violation_list = implode("\n", $violations);
$this->add_violation(
$scss_file_path,
0,
"SCSS manifest module priority must be lower than modules it depends on",
null,
"The SCSS manifest module depends on output from Blade, JavaScript, and Jqhtml modules\n" .
"to determine which SCSS files should have an ID based on matching class selectors.\n\n" .
"Priority violations found:\n" .
$violation_list . "\n\n" .
"Fix: Change Scss_ManifestModule priority() to return a value higher than all dependencies.\n" .
"Remember: Higher number = lower priority (runs later).\n\n" .
"Example: If Blade=15, JavaScript=20, Jqhtml=25, then SCSS should be >25 (e.g., 100).",
'critical'
);
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* Rule to detect .old. files that should not be committed
* Only runs during pre-commit checks
*/
class OldFiles_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-OLD-01';
}
public function get_name(): string
{
return 'Old Files Detection';
}
public function get_description(): string
{
return 'Detects .old files that should not be committed';
}
public function get_file_patterns(): array
{
// Check all files
return ['*'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Only run this rule during pre-commit tests
*/
public function should_run(array $options = []): bool
{
return isset($options['pre-commit-tests']) && $options['pre-commit-tests'] === true;
}
/**
* Check for .old. or .*.old file naming patterns
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip directories outside our scan paths
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
$basename = basename($file_path);
// Check for .old. pattern (e.g., something.old.php)
if (preg_match('/\\.old\\.\\w+$/', $basename)) {
$this->add_violation(
$file_path,
1,
"File uses forbidden .old.(extension) naming pattern",
$basename,
"The .old.(extension) pattern is NOT ALLOWED. Files named like 'file.old.php' " .
"are still treated as .php files and included in bundles as live code.\n\n" .
"SOLUTIONS:\n" .
"1. Rename to '.php.old' or '.js.old' (extension at the end)\n" .
"2. Move to /archived/ directory outside scan paths\n" .
"3. Delete the file if no longer needed",
'critical'
);
}
// Check for .*.old pattern (e.g., something.php.old)
if (preg_match('/\\.\\w+\\.old$/', $basename)) {
$this->add_violation(
$file_path,
1,
"Old file detected - should not be committed",
$basename,
"Files ending in .old should not be committed to the repository.\n\n" .
"SOLUTIONS:\n" .
"1. Move to /archived/ directory outside scan paths\n" .
"2. Delete the file if no longer needed\n" .
"3. Use proper version control (git) to track file history",
'high'
);
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class PackageJson_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PKG-JSON-01';
}
public function get_name(): string
{
return 'Package.json devDependencies Check';
}
public function get_description(): string
{
return 'Ensures package.json files only use dependencies, not devDependencies';
}
public function get_file_patterns(): array
{
return ['package.json'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check package.json files for devDependencies
* RSpade standard: All packages should be in dependencies, not devDependencies
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check package.json files
if (basename($file_path) !== 'package.json') {
return;
}
// Skip node_modules
if (str_contains($file_path, '/node_modules/')) {
return;
}
// Parse JSON
$json = json_decode($contents, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// Invalid JSON - skip check (other rules will catch this)
return;
}
// Check for devDependencies
if (isset($json['devDependencies']) && !empty($json['devDependencies'])) {
$dev_count = count($json['devDependencies']);
$packages = array_keys($json['devDependencies']);
$packages_list = implode(', ', array_slice($packages, 0, 5));
if ($dev_count > 5) {
$packages_list .= ', and ' . ($dev_count - 5) . ' more';
}
$this->add_violation(
$file_path,
0, // JSON files don't have meaningful line numbers for this check
"RSpade Standard Violation: package.json contains {$dev_count} devDependencies. " .
"In RSpade, all packages should be in 'dependencies' to ensure consistent installations. " .
"Found packages: {$packages_list}",
'"devDependencies": { ... }',
"Move all packages from 'devDependencies' to 'dependencies' and remove the 'devDependencies' key entirely. " .
"RSpade makes no distinction between dev and production packages - all software needed for the project should be installed.",
'high'
);
}
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* RedundantIndexActionRule - Checks for unnecessary 'index' action in Route calls
*
* This rule detects when Rsx::Route() or Rsx.Route() is called with 'index' as the
* second parameter, which is redundant since 'index' is the default value.
*
* Example violations:
* - Rsx::Route('Controller') // PHP
* - Rsx.Route('Controller', 'index') // JavaScript
*
* Correct usage:
* - Rsx::Route('Controller') // PHP
* - Rsx.Route('Controller') // JavaScript
*/
class RedundantIndexAction_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'ROUTE-INDEX-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Redundant index Action in Route';
}
/**
* Get rule description
*/
public function get_description(): string
{
return "Detects unnecessary 'index' as second parameter in Route calls since it's the default";
}
/**
* Get file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.php', '*.js', '*.blade.php', '*.jqhtml'];
}
/**
* 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
}
/**
* Get default severity for this rule
*/
public function get_default_severity(): string
{
return 'low'; // This is a style/convention issue, not a functional problem
}
/**
* 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-INDEX-01-EXCEPTION') !== false) {
return;
}
// Pattern to match Rsx::Route() or Rsx.Route() calls with 'index' as second parameter
// Matches both single and double quotes
$pattern = '/(?:Rsx::Route|Rsx\.Route)\s*\(\s*[\'"]([^\'"]+)[\'"],\s*[\'"]index[\'"]\s*\)/';
if (preg_match_all($pattern, $contents, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$full_match = $match[0];
$offset = $match[1];
$controller = $matches[1][$index][0];
// 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 remediation message
$is_php = str_ends_with($file_path, '.php') || str_ends_with($file_path, '.blade.php');
$operator = $is_php ? '::' : '.';
$correct_usage = "Rsx{$operator}Route('{$controller}')";
$remediation = "The 'index' action is the default value and should be omitted.\n\n";
$remediation .= "CURRENT (redundant):\n";
$remediation .= " {$full_match}\n\n";
$remediation .= "CORRECTED (cleaner):\n";
$remediation .= " {$correct_usage}\n\n";
$remediation .= "CONVENTION:\n";
$remediation .= "The second parameter of Rsx{$operator}Route() defaults to 'index'.\n";
$remediation .= "Only specify the action when it's NOT 'index'.\n\n";
$remediation .= "EXAMPLES:\n";
$remediation .= " Rsx{$operator}Route('User_Controller') // Goes to index action\n";
$remediation .= " Rsx{$operator}Route('User_Controller', 'edit') // Goes to edit action\n";
$remediation .= " Rsx{$operator}Route('User_Controller', 'show') // Goes to show action";
$this->add_violation(
$file_path,
$line_number,
"Redundant 'index' action in Route call",
$code_snippet,
$remediation,
'low'
);
}
}
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* Rule to detect incorrect 'resources' directory naming
* Should be 'resource' (singular) not 'resources' (plural)
*/
class ResourceDirectory_CodeQualityRule extends CodeQualityRule_Abstract
{
protected static $checked_directories = [];
public function get_id(): string
{
return 'DIR-RESOURCE-01';
}
public function get_name(): string
{
return 'Resource Directory Naming';
}
public function get_description(): string
{
return 'Enforces singular "resource" directory naming convention';
}
public function get_file_patterns(): array
{
// Check all files to extract directory paths
return ['*'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for 'resources' directory in file path
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip directories outside our scan paths
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip if already inside a 'resource' directory (contents are invisible to framework)
if (preg_match('#/resource/#', $file_path)) {
return;
}
// Check if path contains 'resources' directory
if (preg_match('#/resources/#', $file_path, $matches, PREG_OFFSET_CAPTURE)) {
// Extract the directory path up to and including 'resources'
$offset = $matches[0][1];
$dir_path = substr($file_path, 0, $offset + strlen('/resources'));
// Only report once per directory
if (isset(static::$checked_directories[$dir_path])) {
return;
}
static::$checked_directories[$dir_path] = true;
$this->add_violation(
$file_path,
1,
"Directory named 'resources' (plural) detected - should be 'resource' (singular)",
"Directory: {$dir_path}/",
"The directory name 'resources' is not allowed in RSX.\n\n" .
"USE 'resource' INSTEAD (singular, not plural).\n\n" .
"WHY THIS MATTERS:\n" .
"'resource' is a special directory that is IGNORED by:\n" .
"- The RSpade manifest system\n" .
"- The autoloader\n" .
"- Bundle generation\n" .
"- All Manifest.php functions\n\n" .
"PURPOSE OF resource/ DIRECTORY:\n" .
"Store special-purpose files that are referenced but not executed:\n" .
"- Raw source code (e.g., Bootstrap 5 source)\n" .
"- Supplemental utilities (e.g., Node.js applications)\n" .
"- Documentation files\n" .
"- Assets that should not be bundled\n\n" .
"ACTION REQUIRED:\n" .
"Rename the directory from 'resources' to 'resource'",
'high'
);
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class RootFiles_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-ROOT-01';
}
public function get_name(): string
{
return 'Root Files Check';
}
public function get_description(): string
{
return 'Check for unauthorized files in project root - only whitelisted build configuration files should be in root';
}
public function get_file_patterns(): array
{
// This rule works differently - it checks root directory, not individual files
return [];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check for unauthorized PHP/JS files in project root (from line 1602)
* Only whitelisted build configuration files should be in root
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// This rule needs special handling - it should be called once, not per file
// We'll handle this in the check_root() method instead
}
/**
* Special method to check root files - called once per run
*/
public function check_root(): void
{
$project_root = function_exists('base_path') ? base_path() : '/var/www/html';
$whitelist = function_exists('config') ? config('rsx.code_quality.root_whitelist', []) : [];
// Get all PHP and JS files in root (not subdirectories)
$files = glob($project_root . '/*.{php,js}', GLOB_BRACE);
foreach ($files as $file_path) {
$filename = basename($file_path);
// Skip if whitelisted
if (in_array($filename, $whitelist)) {
continue;
}
$this->add_violation(
$file_path,
0,
"Unauthorized file '{$filename}' found in project root. Only whitelisted build configuration files should exist in the root directory.",
null,
"This file appears to be a one-off test script that should be removed before commit. " .
"LLM agents often create test files in the root directory for testing specific features. " .
"These should be removed or moved to proper test directories. " .
"If this file is legitimately needed in the root, add '{$filename}' to the whitelist in config/rsx.php under 'code_quality.root_whitelist'.",
'medium'
);
}
}
}

View File

@@ -0,0 +1,238 @@
<?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);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
/**
* Check that Route attributes don't use invalid {param} syntax
* RSX uses :param syntax instead of Laravel's {param} syntax
*/
class RouteSyntax_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'ROUTE-SYNTAX-01';
}
public function get_name(): string
{
return 'Route Pattern Syntax Check';
}
public function get_description(): string
{
return 'Ensures route patterns use :param syntax instead of {param}';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
/**
* Run during manifest build for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip files without public static methods
if (!isset($metadata['public_static_methods']) || !is_array($metadata['public_static_methods'])) {
return;
}
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
if (!isset($method_data['attributes']) || !is_array($method_data['attributes'])) {
continue;
}
foreach ($method_data['attributes'] as $attr_name => $attr_instances) {
// Check for Route attribute
if ($attr_name !== 'Route' && !str_ends_with($attr_name, '\\Route')) {
continue;
}
foreach ($attr_instances as $attr_args) {
// Check first argument (the route pattern)
if (!isset($attr_args[0])) {
continue;
}
$pattern = $attr_args[0];
// Check if pattern contains { or }
if (strpos($pattern, '{') !== false || strpos($pattern, '}') !== false) {
$this->throw_invalid_route_syntax($file_path, $method_name, $pattern);
}
}
}
}
}
private function throw_invalid_route_syntax(string $file_path, string $method_name, string $pattern): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Invalid route pattern syntax\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Method: {$method_name}\n";
$error_message .= "Pattern: {$pattern}\n\n";
$error_message .= "RSX routes use :param syntax, not Laravel's {param} syntax.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "Your route pattern contains curly braces { or } which are not supported.\n";
$error_message .= "RSX uses colon-prefixed parameters like :id, :slug, etc.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " #[Route('/users/{id}')]\n";
$error_message .= " #[Route('/posts/{slug}/edit')]\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " #[Route('/users/:id')]\n";
$error_message .= " #[Route('/posts/:slug/edit')]\n\n";
$error_message .= "WHY THIS MATTERS:\n";
$error_message .= "- RSX routing system specifically uses :param syntax\n";
$error_message .= "- The dispatcher expects colon-prefixed parameters\n";
$error_message .= "- Laravel-style {param} patterns won't be recognized\n\n";
$error_message .= "FIX:\n";
$error_message .= "Replace all {param} with :param in your route pattern.\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class RsxCommandsDeprecated_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'RSX-CMD-DEPRECATED-01';
}
public function get_name(): string
{
return 'RSX Commands Deprecated Features Check';
}
public function get_description(): string
{
return 'Checks RSX commands for deprecated features or references';
}
public function get_file_patterns(): array
{
// This rule doesn't use standard file pattern matching
// It scans its own directory when check_rsx_commands() is called
return [];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Standard check method - not used for this rule
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// This rule uses check_rsx_commands() instead
return;
}
/**
* Special method to check RSX commands directory
* Called only when rsx:check is run with default paths
*/
public function check_rsx_commands(): void
{
// Only run when using default paths
if (!($this->config['using_default_paths'] ?? false)) {
return;
}
$base_path = function_exists('base_path') ? base_path() : '/var/www/html';
$commands_dir = $base_path . '/app/RSpade/Commands/Rsx';
// Check if directory exists
if (!is_dir($commands_dir)) {
return;
}
// Scan for PHP files in the RSX commands directory
$files = glob($commands_dir . '/*.php');
foreach ($files as $file_path) {
$this->check_file_for_deprecated($file_path);
}
}
/**
* Check a single file for deprecated references
*/
private function check_file_for_deprecated(string $file_path): void
{
$contents = file_get_contents($file_path);
if ($contents === false) {
return;
}
$lines = explode("\n", $contents);
$filename = basename($file_path);
foreach ($lines as $line_number => $line) {
// Check for the word 'deprecated' (case insensitive)
if (stripos($line, 'deprecated') !== false) {
// Get the actual line number (1-indexed)
$actual_line = $line_number + 1;
// Extract context around the deprecated reference
$context_start = max(0, $line_number - 2);
$context_end = min(count($lines) - 1, $line_number + 2);
$context_lines = array_slice($lines, $context_start, $context_end - $context_start + 1);
$context = implode("\n", $context_lines);
$this->add_violation(
$file_path,
$actual_line,
"Command file '{$filename}' contains reference to 'deprecated'",
trim($line),
$this->get_deprecated_remediation($filename, $line),
'high'
);
}
}
}
/**
* Get remediation message for deprecated references
*/
private function get_deprecated_remediation(string $filename, string $line): string
{
return "DEPRECATED FEATURE REFERENCE IN RSX COMMAND
File: {$filename}
Line containing 'deprecated': {$line}
RSX commands should not contain deprecated features or references to deprecated functionality.
REQUIRED ACTIONS:
1. Remove the deprecated feature or functionality from the command
2. Remove any help text or documentation mentioning deprecated features
3. Update command logic to use current recommended approaches
4. If the entire command is deprecated, consider removing it
COMMON DEPRECATED PATTERNS TO REMOVE:
- Old command aliases marked as deprecated
- Legacy options or flags no longer in use
- References to deprecated framework features
- Outdated help text mentioning deprecated usage
WHY THIS MATTERS:
- Prevents confusion about which features are current
- Reduces maintenance burden of legacy code
- Ensures commands reflect current best practices
- Maintains clean and consistent command interface
If this is documentation about deprecation for historical context,
consider moving it to separate documentation rather than keeping it
in the active command code.";
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class RsxTestFiles_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-RSX-01';
}
public function get_name(): string
{
return 'RSX Test Files Check';
}
public function get_description(): string
{
return 'Check for test files in rsx/ directory and rsx/temp - test files should be in proper test directories';
}
public function get_file_patterns(): array
{
// This rule works differently - it checks rsx directory structure, not individual files
return [];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check for test files in rsx/ directory (from line 1636)
* Test files should be in proper test directories, not loose in rsx/
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// This rule needs special handling - it should be called once, not per file
// We'll handle this in the check_rsx() method instead
}
/**
* Special method to check rsx files - called once per run
*/
public function check_rsx(): void
{
$base_path = function_exists('base_path') ? base_path() : '/var/www/html';
$rsx_dir = $base_path . '/rsx';
$whitelist = function_exists('config') ? config('rsx.code_quality.rsx_test_whitelist', []) : [];
// Check for temp directories only if pre-commit-tests is enabled
$pre_commit_tests = $this->config['pre_commit_tests'] ?? false;
if ($pre_commit_tests) {
// Check both rsx/temp and app/RSpade/temp
$temp_dirs = [
$rsx_dir . '/temp' => 'rsx/temp',
$base_path . '/app/RSpade/temp' => 'app/RSpade/temp'
];
foreach ($temp_dirs as $temp_dir => $temp_name) {
if (is_dir($temp_dir)) {
$temp_files = array_merge(
glob($temp_dir . '/*.php'),
glob($temp_dir . '/*.js'),
glob($temp_dir . '/*')
);
// Remove duplicates and filter out directories
$temp_files = array_unique($temp_files);
$temp_files = array_filter($temp_files, 'is_file');
if (!empty($temp_files)) {
// Files exist in temp directory - report violation
foreach ($temp_files as $file) {
$this->add_violation(
$file,
0,
"File found in {$temp_name} directory. All files in {$temp_name} should be removed prior to commit.",
basename($file),
"The {$temp_name} directory is for temporary test files during development. Remove this file before committing.",
'high' // High severity for pre-commit
);
}
} else {
// Directory exists but is empty - silently remove it
@rmdir($temp_dir);
}
}
}
}
// Get all PHP and JS files in rsx/ (not subdirectories)
$php_files = glob($rsx_dir . '/*.php');
$js_files = glob($rsx_dir . '/*.js');
$files = array_merge($php_files, $js_files);
foreach ($files as $file_path) {
$filename = basename($file_path);
// Check if filename contains 'test' (case insensitive)
if (stripos($filename, 'test') === false) {
continue; // Not a test file
}
// Skip if whitelisted
if (in_array($filename, $whitelist)) {
continue;
}
$this->add_violation(
$file_path,
0,
"Test file '{$filename}' found directly in rsx/ directory. Test files should be organized in proper test subdirectories.",
$filename,
"This appears to be a temporary test file that should be removed before commit. " .
"LLM agents often create test files for verifying specific functionality. " .
"Move this file to a proper test directory (e.g., rsx/rsx_tests/ or rsx/app/tests/) or remove it. " .
"If this file is legitimately needed in rsx/, add '{$filename}' to the whitelist in config/rsx.php under 'code_quality.rsx_test_whitelist'.",
'medium'
);
}
}
}

View File

@@ -0,0 +1,627 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class SubclassNaming_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-SUBCLASS-01';
}
public function get_name(): string
{
return 'Subclass Naming Convention';
}
public function get_description(): string
{
return 'Ensures subclasses end with the same suffix as their parent class';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js'];
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip -temp files
if (str_contains($file_path, '-temp.')) {
return;
}
// Only check files in ./rsx and ./app/RSpade
$is_rsx = str_contains($file_path, '/rsx/');
$is_rspade = str_contains($file_path, '/app/RSpade/');
if (!$is_rsx && !$is_rspade) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if ($is_rspade && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
// Get class and extends information from metadata
if (!isset($metadata['class']) || !isset($metadata['extends'])) {
return;
}
$class_name = $metadata['class'];
$parent_class = $metadata['extends'];
// Skip if no parent class
if (empty($parent_class)) {
return;
}
// Get suffix exempt classes from config
$suffix_exempt_classes = config('rsx.code_quality.suffix_exempt_classes', []);
// Strip FQCN prefix from parent class if present
$parent_class_simple = ltrim($parent_class, '\\');
if (str_contains($parent_class_simple, '\\')) {
$parts = explode('\\', $parent_class_simple);
$parent_class_simple = end($parts);
} else {
$parent_class_simple = $parent_class_simple;
}
// Check if parent class is in the suffix exempt list
// If it is, this child class doesn't need to follow suffix convention
// But any classes extending THIS class will need to follow convention
if (in_array($parent_class_simple, $suffix_exempt_classes)) {
// Don't check suffix for direct children of exempt classes
return;
}
// Skip if extending built-in PHP classes or Laravel framework classes
$built_in_classes = ['Exception', 'RuntimeException', 'InvalidArgumentException', 'LogicException',
'BadMethodCallException', 'DomainException', 'LengthException', 'OutOfBoundsException',
'OutOfRangeException', 'OverflowException', 'RangeException', 'UnderflowException',
'UnexpectedValueException', 'ErrorException', 'Error', 'TypeError', 'ParseError',
'AssertionError', 'ArithmeticError', 'DivisionByZeroError',
'BaseController', 'Controller', 'ServiceProvider', 'Command', 'Model',
'Seeder', 'Factory', 'Migration', 'Request', 'Resource', 'Policy'];
if (in_array($parent_class_simple, $built_in_classes)) {
return;
}
// Also check if the parent of parent is exempt - for deeper inheritance
// E.g., if Widget extends Jqhtml_Component, and Dynamic_Widget extends Widget
// Dynamic_Widget should match Widget's suffix
$parent_of_parent = $this->get_parent_class($parent_class_simple);
if ($parent_of_parent && !in_array($parent_of_parent, $suffix_exempt_classes)) {
// Parent's parent is not exempt, so check suffix based on parent
// Continue with normal suffix checking
}
// Extract the suffix from parent class (use original for suffix extraction)
$suffix = $this->extract_suffix($parent_class);
if (empty($suffix)) {
// This is a violation - parent class name is malformed
$this->add_violation(
$file_path,
0,
"Cannot extract suffix from parent class '$parent_class' - parent class name may be malformed",
"class $class_name extends $parent_class",
$this->get_parent_class_suffix_error($parent_class),
'high'
);
return;
}
// Check if child class is abstract based on metadata or class name
$child_is_abstract = isset($metadata['is_abstract']) ? $metadata['is_abstract'] : str_ends_with($class_name, '_Abstract');
// CRITICAL LOGIC: If parent suffix contains "Abstract" and child is NOT abstract,
// remove "Abstract" from the expected suffix
// Example: AbstractRule (parent) → *_Rule (child, not *_AbstractRule)
if (!$child_is_abstract && str_contains($suffix, 'Abstract')) {
// Remove "Abstract" from suffix for non-abstract children
$suffix = str_replace('Abstract', '', $suffix);
// Clean up any double underscores or leading/trailing underscores
$suffix = trim($suffix, '_');
if (empty($suffix)) {
// If suffix becomes empty after removing Abstract, skip validation
// This handles edge cases like a class named just "Abstract"
return;
}
}
// Special handling for abstract classes
if ($child_is_abstract) {
// This is an abstract class - it should have the parent suffix as second-to-last term
$result = $this->check_abstract_class_naming($class_name, $suffix);
if (!$result['valid']) {
$this->add_violation(
$file_path,
0,
$result['message'],
"class $class_name extends $parent_class",
$result['remediation'],
'high'
);
}
return; // Don't check filename for abstract classes - different rules apply
}
// Check if child class suffix contains parent suffix (compound suffix issue)
$child_suffix = $this->extract_child_suffix($class_name);
$compound_suffix_issue = false;
if ($child_suffix && $child_suffix !== $suffix && str_ends_with($child_suffix, $suffix)) {
// Child has compound suffix like ServiceProvider when parent is Provider
$compound_suffix_issue = true;
}
// Check 1: Class name must end with appropriate suffix
$class_name_valid = $this->check_class_name_suffix($class_name, $suffix, $is_rsx);
if (!$class_name_valid || $compound_suffix_issue) {
// Determine expected suffix based on location and Rsx prefix
$expected_suffix = $this->get_expected_suffix($suffix, $is_rsx);
$this->add_violation(
$file_path,
0,
"Class '$class_name' extends '$parent_class' but doesn't end with '_{$expected_suffix}'",
"class $class_name extends $parent_class",
$this->get_class_name_remediation($class_name, $parent_class, $suffix, $is_rsx, $compound_suffix_issue, $child_suffix),
'high'
);
return; // Don't check filename if class name is wrong
}
// Check 2: Filename must follow convention (only if class name is valid)
$filename = basename($file_path);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
if (!$this->check_filename_convention($filename, $class_name, $suffix, $extension)) {
$expected_suffix = $this->get_expected_suffix($suffix, $is_rsx);
$this->add_violation(
$file_path,
0,
"Filename '$filename' doesn't follow naming convention for class '$class_name'",
"File: $filename",
$this->get_filename_remediation($class_name, $filename, $expected_suffix, $extension, $is_rsx),
'medium'
);
}
}
private function extract_suffix(string $parent_class): string
{
// Strip FQCN prefix if present
$parent_class = ltrim($parent_class, '\\');
if (str_contains($parent_class, '\\')) {
$parts = explode('\\', $parent_class);
$parent_class = end($parts);
}
// Split by underscores
$parts = explode('_', $parent_class);
// If no underscores, check for special cases
if (count($parts) === 1) {
// For single-word classes, return the whole name as suffix
// This includes cases like "AbstractRule", "BaseController", etc.
return $parent_class;
}
// Find the last part that is NOT "Abstract"
for ($i = count($parts) - 1; $i >= 0; $i--) {
if ($parts[$i] !== 'Abstract') {
// If this is a multi-part suffix like ManifestBundle_Abstract
// we need to get everything from this point backwards until we hit a proper boundary
// Special case: if the parent ends with _Abstract, get everything before it
if (str_ends_with($parent_class, '_Abstract')) {
$pos = strrpos($parent_class, '_Abstract');
$before_abstract = substr($parent_class, 0, $pos);
// Get the last "word" which could be multi-part
$before_parts = explode('_', $before_abstract);
// If it's something like Manifest_Bundle_Abstract, suffix is "Bundle"
// If it's something like ManifestBundle_Abstract, suffix is "ManifestBundle"
if (count($before_parts) > 0) {
return $before_parts[count($before_parts) - 1];
}
}
return $parts[$i];
}
}
// If we couldn't extract a suffix, return empty string
// This will trigger a violation in the check method
return '';
}
private function check_class_name_suffix(string $class_name, string $suffix, bool $is_rsx): bool
{
// Special case: Allow class name to be the same as the suffix
// e.g., Main extending Main_Abstract
if ($class_name === $suffix) {
return true;
}
// Special handling for Rsx-prefixed suffixes
if (str_starts_with($suffix, 'Rsx')) {
// Remove 'Rsx' prefix to get the base suffix
$base_suffix = substr($suffix, 3);
if ($is_rsx) {
// In /rsx/ directory: child class should use suffix WITHOUT 'Rsx'
// e.g., Demo_Bundle extends Rsx_Bundle_Abstract ✓
return str_ends_with($class_name, '_' . $base_suffix);
} else {
// In /app/RSpade/ directory: child class can use suffix WITH or WITHOUT 'Rsx'
// e.g., Cool_Rule extends RsxRule ✓ OR Cool_RsxRule extends RsxRule ✓
return str_ends_with($class_name, '_' . $suffix) ||
str_ends_with($class_name, '_' . $base_suffix);
}
}
// Standard suffix handling (non-Rsx prefixed)
// Class name must end with _{Suffix}
$expected_ending = '_' . $suffix;
return str_ends_with($class_name, $expected_ending);
}
private function check_filename_convention(string $filename, string $class_name, string $suffix, string $extension): bool
{
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
// Special case: When class name equals suffix (e.g., Main extending Main_Abstract)
// Allow either exact match (Main.php) or lowercase (main.php)
if ($class_name === $suffix) {
if ($filename_without_ext === $class_name || $filename_without_ext === strtolower($class_name)) {
return true;
}
}
// Two valid patterns:
// 1. Exact match to class name: User_Model.php
if ($filename_without_ext === $class_name) {
return true;
}
// 2. Ends with underscore + lowercase suffix: anything_model.php
// For Rsx-prefixed suffixes, use the base suffix (without Rsx) in lowercase
$actual_suffix = str_starts_with($suffix, 'Rsx') ? substr($suffix, 3) : $suffix;
$lowercase_suffix = strtolower($actual_suffix);
if (str_ends_with($filename_without_ext, '_' . $lowercase_suffix)) {
return true;
}
return false;
}
private function extract_child_suffix(string $class_name): string
{
$parts = explode('_', $class_name);
if (count($parts) > 1) {
return $parts[count($parts) - 1];
}
return '';
}
private function get_class_name_remediation(string $class_name, string $parent_class, string $suffix, bool $is_rsx, bool $compound_suffix_issue = false, string $child_suffix = ''): string
{
// Check if this is about Abstract suffix handling
$is_abstract_suffix_issue = str_contains($parent_class, 'Abstract') && !str_ends_with($class_name, '_Abstract');
// Try to suggest a better class name
$suggested_class = $this->suggest_class_name($class_name, $suffix, $is_rsx, $compound_suffix_issue, $child_suffix);
$expected_suffix = $this->get_expected_suffix($suffix, $is_rsx);
// Check if this involves Laravel classes
$laravel_classes = ['BaseController', 'Controller', 'ServiceProvider', 'Command', 'Model',
'Seeder', 'Factory', 'Migration', 'Request', 'Resource', 'Policy'];
$is_laravel_involved = false;
foreach ($laravel_classes as $laravel_class) {
if (str_contains($parent_class, $laravel_class) || str_contains($class_name, $laravel_class)) {
$is_laravel_involved = true;
break;
}
}
$compound_suffix_section = '';
if ($compound_suffix_issue && $child_suffix) {
$compound_suffix_section = "\n\nCOMPOUND SUFFIX DETECTED:\nYour class uses '$child_suffix' when the parent uses '$suffix'.\nThis creates ambiguity. The suffix should be split with underscores.\nFor example: 'ServiceProvider' should become 'Service_Provider'\n";
}
$laravel_section = '';
if ($is_laravel_involved) {
$laravel_section = "\n\nLARAVEL CLASS DETECTED:\nEven though this involves Laravel framework classes, the RSX naming convention STILL APPLIES.\nRSX enforces its own conventions uniformly across all code.\nLaravel's PascalCase conventions are overridden by RSX's underscore notation.\n";
}
$abstract_handling_note = '';
if ($is_abstract_suffix_issue) {
$abstract_handling_note = "\n\nABSTRACT SUFFIX HANDLING:\n" .
"When a parent class contains 'Abstract' in its name (like '$parent_class'),\n" .
"non-abstract child classes should use the suffix WITHOUT 'Abstract'.\n" .
"This is because concrete implementations should not have 'Abstract' in their names.\n";
}
return "CLASS NAMING CONVENTION VIOLATION" . $compound_suffix_section . $laravel_section . $abstract_handling_note . "
Class '$class_name' extends '$parent_class' but doesn't follow RSX naming conventions.
REQUIRED SUFFIX: '$expected_suffix'
All classes extending '$parent_class' must end with '_{$expected_suffix}'
RSX NAMING PATTERN:
RSpade uses underscore notation for class names, separating major conceptual parts:
CORRECT: User_Downloads_Model, Site_User_Model, Php_ManifestModule
WRONG: UserDownloadsModel, SiteUserModel, PhpManifestModule
SUFFIX CONVENTION:
The suffix (last part after underscore) can be multi-word without underscores to describe the class type:
- 'Model' suffix for database models
- 'Controller' suffix for controllers
- 'ManifestModule' suffix for manifest module implementations
- 'BundleProcessor' suffix for bundle processors
These multi-word suffixes act as informal type declarations (e.g., Php_ManifestModule indicates a PHP implementation of a manifest module).
RSX PREFIX SPECIAL RULE:
- In /rsx/ directory: If parent class has 'Rsx' prefix (e.g., Rsx_Bundle_Abstract), child uses suffix WITHOUT 'Rsx' (e.g., Demo_Bundle)
- In /app/RSpade/ directory: Child can use suffix WITH or WITHOUT 'Rsx' prefix (e.g., Cool_Rule or Cool_RsxRule extending RsxRule)
SUGGESTED CLASS NAME: $suggested_class
The filename should also end with:
- For files in /rsx: underscore + suffix in lowercase (e.g., user_downloads_model.php)
- For files in /app/RSpade: suffix matching class name case (e.g., User_Downloads_Model.php)
REMEDIATION STEPS:
1. Rename class from '$class_name' to '$suggested_class'
2. Update filename to match convention
3. Update all references to this class throughout the codebase
WHY THIS MATTERS:
- Enables automatic class discovery and loading
- Makes inheritance relationships immediately clear
- Maintains consistency across the entire codebase
- Supports framework introspection capabilities";
}
private function get_filename_remediation(string $class_name, string $filename, string $suffix, string $extension, bool $is_rsx): string
{
// Suggest filenames based on directory
$lowercase_suggestion = strtolower(str_replace('_', '_', $class_name)) . ".$extension";
$exact_suggestion = $class_name . ".$extension";
$recommended = $is_rsx ? $lowercase_suggestion : $exact_suggestion;
return "FILENAME CONVENTION VIOLATION
Filename '$filename' doesn't follow naming convention for class '$class_name'
VALID FILENAME PATTERNS:
1. Underscore + lowercase suffix: *_" . strtolower($suffix) . ".$extension
2. Exact class name match: $class_name.$extension
CURRENT FILE: $filename
CURRENT CLASS: $class_name
RECOMMENDED FIX:
- For /rsx directory: $lowercase_suggestion
- For /app/RSpade directory: $exact_suggestion
Note: Both patterns are valid in either directory, but these are the conventions.
EXAMPLES OF VALID FILENAMES:
- user_model.$extension (underscore + lowercase suffix)
- site_user_model.$extension (underscore + lowercase suffix)
- $class_name.$extension (exact match)
WHY THIS MATTERS:
- Enables predictable file discovery
- Maintains consistency with directory conventions
- Supports autoloading mechanisms";
}
private function suggest_class_name(string $current_name, string $suffix, bool $is_rsx, bool $compound_suffix_issue = false, string $child_suffix = ''): string
{
// Handle compound suffix issue specially
if ($compound_suffix_issue && $child_suffix) {
// Split the compound suffix with underscores
// E.g., ServiceProvider -> Service_Provider
$split_suffix = $this->split_compound_suffix($child_suffix, $suffix);
if ($split_suffix) {
// Replace the compound suffix with the split version
$base = substr($current_name, 0, -strlen($child_suffix));
return $base . $split_suffix;
}
}
// Determine the suffix to use based on location and Rsx prefix
$target_suffix = $this->get_expected_suffix($suffix, $is_rsx);
// If it already ends with the target suffix (but without underscore), add underscore
if (str_ends_with($current_name, $target_suffix) && !str_ends_with($current_name, '_' . $target_suffix)) {
$without_suffix = substr($current_name, 0, -strlen($target_suffix));
// Convert camelCase to snake_case if needed
$snake_case = preg_replace('/([a-z])([A-Z])/', '$1_$2', $without_suffix);
return $snake_case . '_' . $target_suffix;
}
// Otherwise, just append _Suffix
// Convert the current name to proper underscore notation first
$snake_case = preg_replace('/([a-z])([A-Z])/', '$1_$2', $current_name);
return $snake_case . '_' . $target_suffix;
}
private function get_expected_suffix(string $suffix, bool $is_rsx): string
{
// For Rsx-prefixed suffixes in /rsx/ directory, use suffix without 'Rsx'
if (str_starts_with($suffix, 'Rsx') && $is_rsx) {
return substr($suffix, 3);
}
// Otherwise use the full suffix
return $suffix;
}
private function get_parent_class_suffix_error(string $parent_class): string
{
return "PARENT CLASS SUFFIX EXTRACTION ERROR
Unable to extract a valid suffix from parent class '$parent_class'.
This is an unexpected situation that indicates either:
1. The parent class name has an unusual format that the rule doesn't handle
2. The naming rule logic needs to be updated to handle this case
EXPECTED PARENT CLASS FORMATS:
- Classes with underscores: Last part after underscore is the suffix (e.g., 'Rsx_Model_Abstract' suffix 'Model')
- Classes ending with 'Abstract': Remove 'Abstract' to get suffix (e.g., 'RuleAbstract' suffix 'Rule')
- Classes ending with '_Abstract': Part before '_Abstract' is suffix (e.g., 'Model_Abstract' suffix 'Model')
- Multi-word suffixes: 'ManifestModule_Abstract' suffix 'ManifestModule'
PLEASE REVIEW:
1. Check if the parent class name follows RSX naming conventions
2. If the parent class name is valid but unusual, the SubclassNamingRule may need updating
3. Consider renaming the parent class to follow standard patterns
This violation indicates a framework-level issue that needs attention from the development team.";
}
private function split_compound_suffix(string $compound, string $parent_suffix): string
{
// If compound ends with parent suffix, split it
if (str_ends_with($compound, $parent_suffix)) {
$prefix = substr($compound, 0, -strlen($parent_suffix));
if ($prefix) {
return $prefix . '_' . $parent_suffix;
}
}
return '';
}
private function check_abstract_class_naming(string $class_name, string $parent_suffix): array
{
// Strip Base prefix from parent suffix if present
if (str_starts_with($parent_suffix, 'Base')) {
$parent_suffix = substr($parent_suffix, 4);
}
// Get the parts of the abstract class name
$parts = explode('_', $class_name);
// Must have at least 2 parts (Something_Abstract)
if (count($parts) < 2) {
return [
'valid' => false,
'message' => "Abstract class '$class_name' doesn't follow underscore notation",
'remediation' => "Abstract classes must use underscore notation and end with '_Abstract'.\nExample: User_Controller_Abstract, Rsx_Model_Abstract"
];
}
// Last part must be 'Abstract'
if ($parts[count($parts) - 1] !== 'Abstract') {
return [
'valid' => false,
'message' => "Class '$class_name' appears to be abstract but doesn't end with '_Abstract'",
'remediation' => "All abstract classes must end with '_Abstract'.\nSuggested: " . implode('_', array_slice($parts, 0, -1)) . "_Abstract"
];
}
// If only 2 parts (Something_Abstract), that's valid for root abstracts
if (count($parts) == 2) {
return ['valid' => true];
}
// For multi-part names, check that second-to-last term matches parent suffix
$second_to_last = $parts[count($parts) - 2];
if ($second_to_last !== $parent_suffix) {
// Build suggested name
$suggested_parts = array_slice($parts, 0, -2); // Everything except last 2 parts
$suggested_parts[] = $parent_suffix;
$suggested_parts[] = 'Abstract';
$suggested_name = implode('_', $suggested_parts);
return [
'valid' => false,
'message' => "Abstract class '$class_name' doesn't properly indicate it extends a '$parent_suffix' type",
'remediation' => "ABSTRACT CLASS NAMING CONVENTION\n\n" .
"Abstract classes must:\n" .
"1. End with '_Abstract'\n" .
"2. Have the parent type as the second-to-last term\n\n" .
"Current: $class_name\n" .
"Expected pattern: *_{$parent_suffix}_Abstract\n" .
"Suggested: $suggested_name\n\n" .
"This makes the inheritance chain clear:\n" .
"- Parent provides: $parent_suffix functionality\n" .
"- This class: Abstract extension of $parent_suffix\n\n" .
"Note: If the parent class starts with 'Base' (e.g., BaseController),\n" .
"we strip 'Base' to get the actual type (Controller)."
];
}
return ['valid' => true];
}
/**
* Get the parent class of a given class from manifest or other sources
*/
private function get_parent_class(string $class_name): ?string
{
// Try to get from manifest
try {
$metadata = \App\RSpade\Core\Manifest\Manifest::php_get_metadata_by_class($class_name);
if (!empty($metadata) && isset($metadata['extends'])) {
$parent = $metadata['extends'];
// Strip namespace if present
if (str_contains($parent, '\\')) {
$parts = explode('\\', $parent);
return end($parts);
}
return $parent;
}
} catch (\RuntimeException $e) {
// Class not in manifest (e.g., framework classes like DatabaseSessionHandler)
// Return null since we can't check parent of external classes
return null;
}
return null;
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class TempFiles_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'FILE-TEMP-01';
}
public function get_name(): string
{
return 'Temporary Files Check';
}
public function get_description(): string
{
return 'Check for temporary files ending in -temp that should be removed before commit';
}
public function get_file_patterns(): array
{
return ['*']; // Check all files
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for files ending in -temp before their extension
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only run during pre-commit tests
$pre_commit_tests = $this->config['pre_commit_tests'] ?? false;
if (!$pre_commit_tests) {
return;
}
// Check if filename contains -temp before the extension
$filename = basename($file_path);
// Match files like test-temp.php, module-temp.js, etc.
if (preg_match('/^(.+)-temp(\.[^.]+)?$/', $filename, $matches)) {
$this->add_violation(
$file_path,
0,
"Temporary file '{$filename}' detected. Files ending in '-temp' should be removed before commit.",
$filename,
"The '-temp' suffix indicates this is a temporary file for testing or development.\n" .
"These files should not be committed to the repository.\n\n" .
"Options:\n" .
"1. Remove the file if it's no longer needed\n" .
"2. Rename the file without '-temp' if it should be kept\n" .
"3. Move to a proper test directory if it's a test file",
'high'
);
}
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class BundleIncludePath_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-BUNDLE-02';
}
public function get_name(): string
{
return 'Bundle Include Path Convention';
}
public function get_description(): string
{
return 'Bundles should include __DIR__ or their relative path in their includes';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'low';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a PHP class file
if (!isset($metadata['class'])) {
return;
}
// Only check files in ./rsx/ directory, trust framework authors for app/RSpade
if (!str_starts_with($file_path, base_path() . '/rsx/')) {
return;
}
// Check if class extends Rsx_Bundle_Abstract
$extends = $metadata['extends'] ?? '';
if ($extends !== 'Rsx_Bundle_Abstract' &&
$extends !== 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract') {
return;
}
// Get relative path from base
$relative_path = str_replace(base_path() . '/', '', $file_path);
$dir_path = dirname($relative_path);
// Check if bundle includes its own directory in the include array
// Look for the define() method
if (!preg_match('/public\s+static\s+function\s+define\s*\(\s*\)\s*:\s*array\s*\{(.*?)\}/s', $contents, $matches)) {
return; // Can't find define method
}
$define_content = $matches[1];
// Look for include array
if (!preg_match("/['\"]include['\"]\s*=>\s*\[(.*?)\]/s", $define_content, $include_matches)) {
return; // No include array found
}
$include_content = $include_matches[1];
// Check if it references __DIR__ or the directory path
$has_dir_reference = false;
// Check for __DIR__ usage
if (str_contains($include_content, '__DIR__')) {
$has_dir_reference = true;
}
// Check for the relative directory path (e.g., 'rsx/app/demo')
if (str_contains($include_content, "'" . $dir_path) ||
str_contains($include_content, '"' . $dir_path)) {
$has_dir_reference = true;
}
if (!$has_dir_reference) {
// Get bundle class name for better message
$class_name = $metadata['class'] ?? basename($file_path, '.php');
$this->add_violation(
$file_path,
0,
"Bundle {$class_name} should include its own directory in the 'include' array",
null,
"Add '__DIR__' or '{$dir_path}' to the bundle's include array to ensure all module files are included.\n" .
"Note: This is a convention rather than a hard requirement. If your bundle intentionally doesn't need " .
"to include its own directory, add the following comment to grant an exception: @CONV-BUNDLE-02-EXCEPTION",
$this->get_default_severity()
);
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class BundleLocation_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-BUNDLE-01';
}
public function get_name(): string
{
return 'Bundle Location Convention';
}
public function get_description(): string
{
return 'Bundles should be in ./rsx/app or ./rsx/app/(module)/ but not deeper';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'convention';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a PHP class file
if (!isset($metadata['class'])) {
return;
}
// Check if class extends Rsx_Bundle_Abstract
$extends = $metadata['extends'] ?? '';
if ($extends !== 'Rsx_Bundle_Abstract' &&
$extends !== 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract') {
return;
}
// Get relative path from base
$relative_path = str_replace(base_path() . '/', '', $file_path);
// Check if it's in rsx/app directory
if (!str_starts_with($relative_path, 'rsx/app/')) {
return; // Bundle is not in rsx/app, that's ok (could be in rsx/lib etc)
}
// Count directory levels after rsx/app/
$after_app = substr($relative_path, strlen('rsx/app/'));
$parts = explode('/', $after_app);
// If more than 2 levels deep (module/feature/file.php), it's a violation
if (count($parts) > 2) {
$this->add_violation(
$file_path,
0,
'Bundle class should be in ./rsx/app/ or ./rsx/app/(module)/ but not in feature subdirectories',
null,
'Move this bundle to ./rsx/app/ if used globally, or to ./rsx/app/' . $parts[0] . '/ if module-specific',
'convention'
);
}
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* FilenameRedundantPrefix_CodeQualityRule - Detects unnecessarily long filenames
*
* Suggests using short filenames when the directory structure already contains the prefix.
*/
class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-FILENAME-01';
}
public function get_name(): string
{
return 'Filename Redundant Prefix Convention';
}
public function get_description(): string
{
return 'Suggests using short filenames when directory structure contains the prefix';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js', '*.jqhtml', '*.blade.php'];
}
public function get_default_severity(): string
{
return 'convention';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx or ./app/RSpade
$relative_path = str_replace(base_path() . '/', '', $file_path);
$is_rsx = str_starts_with($relative_path, 'rsx/');
$is_rspade = str_starts_with($relative_path, 'app/RSpade/');
if (!$is_rsx && !$is_rspade) {
return;
}
$extension = $metadata['extension'] ?? '';
$filename = basename($file_path);
// Check PHP/JS files with classes
if (isset($metadata['class'])) {
$this->check_class_redundancy($relative_path, $metadata['class'], $extension, $filename, $is_rspade);
}
// Check blade.php files with @rsx_id
if ($extension === 'blade.php' && isset($metadata['id'])) {
$this->check_blade_redundancy($relative_path, $metadata['id'], $filename, $is_rspade);
}
// Check jqhtml files with Define:
if ($extension === 'jqhtml' && isset($metadata['id'])) {
$this->check_jqhtml_redundancy($relative_path, $metadata['id'], $filename, $is_rspade);
}
}
private function check_class_redundancy(string $file, string $class_name, string $extension, string $filename, bool $is_rspade): void
{
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($class_name, $dir_path);
if ($short_name === null) {
return; // No short name available
}
// Check if current filename is the full name (redundant)
$is_full_name = $is_rspade
? $filename_without_ext === $class_name
: strtolower($filename_without_ext) === strtolower($class_name);
if (!$is_full_name) {
return; // Not using full name
}
// Check if short filename would be available
$short_filename = $is_rspade ? $short_name . '.' . $extension : strtolower($short_name) . '.' . $extension;
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return; // Short name already taken
}
$this->add_violation(
$file,
1,
"Filename contains redundant prefix already represented in directory structure",
"class $class_name",
"Directory structure already contains the class name prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The class name must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/demo_controller.php → rsx/app/demo/controller.php\n" .
" (but class Demo_Controller remains Demo_Controller)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
private function check_blade_redundancy(string $file, string $rsx_id, string $filename, bool $is_rspade): void
{
$filename_without_blade = str_replace('.blade.php', '', $filename);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($rsx_id, $dir_path);
if ($short_name === null) {
return;
}
$is_full_name = $is_rspade
? $filename_without_blade === $rsx_id
: strtolower($filename_without_blade) === strtolower($rsx_id);
if (!$is_full_name) {
return;
}
$short_filename = $is_rspade ? $short_name . '.blade.php' : strtolower($short_name) . '.blade.php';
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return;
}
$this->add_violation(
$file,
1,
"Blade filename contains redundant prefix already represented in directory structure",
"@rsx_id('$rsx_id')",
"Directory structure already contains the @rsx_id prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The @rsx_id must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/sections/demo_sections_cards.blade.php → cards.blade.php\n" .
" (but @rsx_id('demo.sections.cards') remains demo.sections.cards)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
private function check_jqhtml_redundancy(string $file, string $component_name, string $filename, bool $is_rspade): void
{
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($component_name, $dir_path);
if ($short_name === null) {
return;
}
$is_full_name = $is_rspade
? $filename_without_ext === $component_name
: strtolower($filename_without_ext) === strtolower($component_name);
if (!$is_full_name) {
return;
}
$short_filename = $is_rspade ? $short_name . '.jqhtml' : strtolower($short_name) . '.jqhtml';
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return;
}
$this->add_violation(
$file,
1,
"Jqhtml filename contains redundant prefix already represented in directory structure",
"<Define:$component_name>",
"Directory structure already contains the component name prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The component name must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/components/demo_card.jqhtml → card.jqhtml\n" .
" (but <Define:Demo_Card> remains Demo_Card)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
/**
* Extract short name from class/id if directory structure matches prefix
* Returns null if directory structure doesn't match or if short name rules aren't met
*
* Rules:
* - Original name must have 3+ segments for short name to be allowed (2-segment names must use full name)
* - Short name must have 2+ segments (exception: if original was 1 segment, short can be 1 segment)
*/
private function extract_short_name(string $full_name, string $dir_path): ?string
{
$name_parts = explode('_', $full_name);
$original_segment_count = count($name_parts);
// If original name has exactly 2 segments, short name is NOT allowed
if ($original_segment_count === 2) {
return null;
}
// If only 1 segment, no prefix to match
if ($original_segment_count === 1) {
return null;
}
// Split directory path into parts and re-index
$dir_parts = array_values(array_filter(explode('/', $dir_path)));
// Find the maximum number of consecutive matches between end of dir_parts and start of name_parts
$matched_parts = 0;
$max_possible = min(count($dir_parts), count($name_parts) - 1);
// Try to match last N dir parts with first N name parts
for ($num_to_check = $max_possible; $num_to_check > 0; $num_to_check--) {
$all_match = true;
for ($i = 0; $i < $num_to_check; $i++) {
$dir_idx = count($dir_parts) - $num_to_check + $i;
if (strtolower($dir_parts[$dir_idx]) !== strtolower($name_parts[$i])) {
$all_match = false;
break;
}
}
if ($all_match) {
$matched_parts = $num_to_check;
break;
}
}
if ($matched_parts === 0) {
return null;
}
// Calculate the short name
$short_parts = array_slice($name_parts, $matched_parts);
$short_segment_count = count($short_parts);
// Validate short name segment count
// Short name must have 2+ segments (unless original was 1 segment, which we already excluded above)
if ($short_segment_count < 2) {
return null; // Short name would be too short
}
return implode('_', $short_parts);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class LayoutLocation_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-LAYOUT-01';
}
public function get_name(): string
{
return 'Layout File Location Convention';
}
public function get_description(): string
{
return 'Layout blade files in ./rsx/ must be within a module directory (./rsx/app/(module)/ or deeper)';
}
public function get_file_patterns(): array
{
return ['*.blade.php'];
}
public function get_default_severity(): string
{
return 'convention';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Check if filename ends with layout.blade.php
if (!preg_match('/_layout\.blade\.php$/', $file_path)) {
return;
}
// Get relative path from base
$relative_path = str_replace(base_path() . '/', '', $file_path);
// Only check layouts in rsx/ directory (not app/RSpade)
if (!str_starts_with($relative_path, 'rsx/')) {
return;
}
// Check if it's in rsx/app directory
if (!str_starts_with($relative_path, 'rsx/app/')) {
$this->add_violation(
$file_path,
0,
'Layout file must be within ./rsx/app/ directory',
null,
'Move this layout to ./rsx/app/(module)/ or a subdirectory within a module',
'convention'
);
return;
}
// Count directory levels after rsx/app/
$after_app = substr($relative_path, strlen('rsx/app/'));
$parts = explode('/', $after_app);
// Layout must be at least 2 levels deep: module/file.php
if (count($parts) < 2) {
$this->add_violation(
$file_path,
0,
'Layout file must be within a module directory, not directly in ./rsx/app/',
null,
'Move this layout to ./rsx/app/(module)/ or a subdirectory within a module',
'convention'
);
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\Core\Manifest\Manifest;
class OneBundlePerModule_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-BUNDLE-03';
}
public function get_name(): string
{
return 'One Bundle Per Module Directory Convention';
}
public function get_description(): string
{
return 'Module directories should have only one bundle (./rsx/app root can have multiple)';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Whether this rule is called during manifest scan
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* bundle organization is a critical framework convention for module structure.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a PHP class file
if (!isset($metadata['class'])) {
return;
}
// Check if class extends Rsx_Bundle_Abstract
$extends = $metadata['extends'] ?? '';
if ($extends !== 'Rsx_Bundle_Abstract' &&
$extends !== 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract') {
return;
}
// Get relative path from base
$relative_path = str_replace(base_path() . '/', '', $file_path);
$dir_path = dirname($relative_path);
// Skip if bundle is directly in rsx/app (they can have multiple)
if ($dir_path === 'rsx/app') {
return;
}
// Skip if not in rsx/app
if (!str_starts_with($dir_path, 'rsx/app/')) {
return;
}
// Check the manifest for other bundles in the same directory
$manifest = Manifest::get_all();
$bundles_in_same_dir = [];
foreach ($manifest as $path => $file_metadata) {
// Check if it's a PHP file in the same directory
if (dirname($path) !== $dir_path) {
continue;
}
// Check if it's a bundle class
if (isset($file_metadata['class']) && isset($file_metadata['extends'])) {
$file_extends = $file_metadata['extends'];
if ($file_extends === 'Rsx_Bundle_Abstract' ||
$file_extends === 'App\\RSpade\\Core\\Bundle\\Rsx_Bundle_Abstract') {
$bundles_in_same_dir[] = basename($path, '.php');
}
}
}
// If there's more than one bundle in this directory, throw an exception
if (count($bundles_in_same_dir) > 1) {
$error_message = "Code Quality Violation (CONV-BUNDLE-03) - Multiple Bundles in Same Directory\n\n";
$error_message .= "Module directory '{$dir_path}' has multiple bundle files:\n";
foreach ($bundles_in_same_dir as $bundle_name) {
$error_message .= " - {$bundle_name}.php\n";
}
$error_message .= "\nCRITICAL: Each module directory should have only ONE bundle.\n\n";
$error_message .= "Resolution:\n";
$error_message .= "1. Consolidate these bundles into a single bundle file\n";
$error_message .= "2. OR move extra bundles to their own module directories\n";
$error_message .= "3. OR move them to ./rsx/app/ (which allows multiple bundles)\n\n";
$error_message .= "This convention ensures clean module organization and predictable bundle loading.";
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
$file_path,
1
);
}
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class AjaxReturnValue_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-AJAX-02';
}
public function get_name(): string
{
return 'AJAX Return Value Property Check';
}
public function get_description(): string
{
return "Detects unnecessary access to _ajax_return_value property in AJAX responses";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check JavaScript files for _ajax_return_value property access
* Ajax.call() already unwraps the response, so accessing _ajax_return_value is unnecessary
* Instead of: data._ajax_return_value.user_id
* Should use: data.user_id
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip Ajax.js itself
if (str_ends_with($file_path, '/Ajax.js')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ or app/RSpade/
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for _ajax_return_value usage
if (str_contains($line, '_ajax_return_value')) {
// Try to extract context for better suggestion
$suggestion = "Ajax.call() automatically unwraps the server response. ";
// Check if we can detect the specific property being accessed
if (preg_match('/(\w+)\._ajax_return_value\.(\w+)/', $line, $matches)) {
$variable = $matches[1];
$property = $matches[2];
$suggestion .= "Instead of '{$variable}._ajax_return_value.{$property}', ";
$suggestion .= "use '{$variable}.{$property}' directly.";
} elseif (preg_match('/(\w+)\._ajax_return_value/', $line, $matches)) {
$variable = $matches[1];
$suggestion .= "Instead of '{$variable}._ajax_return_value', ";
$suggestion .= "use '{$variable}' directly - it already contains the unwrapped response.";
} else {
$suggestion .= "The response from Ajax.call() is already unwrapped, ";
$suggestion .= "so you can access properties directly without the _ajax_return_value wrapper.";
}
$this->add_violation(
$file_path,
$line_number,
"Unnecessary access to '_ajax_return_value' property detected.",
trim($line),
$suggestion,
'medium'
);
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
use App\RSpade\Core\Manifest\Manifest;
/**
* Check decorator usage and enforce whitelisting rules
* - Functions/methods marked with @decorator become whitelisted
* - Global functions can only use @decorator
* - Static and instance methods can only use whitelisted decorators
* - Checks for duplicate global function names
* - Checks for duplicate global const names
* - Checks for conflicts between global function and const names
*/
class DecoratorUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
private array $decorator_whitelist = [];
private array $all_global_functions = [];
private array $all_global_constants = [];
private array $all_global_names = []; // Combined functions and constants for conflict checking
public function get_id(): string
{
return 'JS-DECORATOR-01';
}
public function get_name(): string
{
return 'JavaScript Decorator Usage Check';
}
public function get_description(): string
{
return 'Validates JavaScript decorator usage and whitelisting (static and instance methods)';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
/**
* Run during manifest build for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only process during manifest-time when we have all files
// This rule needs to scan all files first to build the whitelist
// So we run it once at the end of manifest building
static $already_run = false;
if ($already_run) {
return;
}
// On the first JavaScript file, process all files
if (!empty($metadata) && $metadata['extension'] === 'js') {
$this->process_all_files();
$already_run = true;
}
}
private function process_all_files(): void
{
// Get all files from manifest
$files = Manifest::get_all();
// Step 1: Build decorator whitelist and collect all global functions
foreach ($files as $path => $metadata) {
// Skip non-JavaScript files
if (($metadata['extension'] ?? '') !== 'js') {
continue;
}
// Collect global function names for uniqueness check
if (!empty($metadata['global_function_names'])) {
foreach ($metadata['global_function_names'] as $func_name) {
if (isset($this->all_global_functions[$func_name])) {
// Duplicate function name
$existing_file = $this->all_global_functions[$func_name];
$this->throw_duplicate_global($func_name, 'function', $existing_file, $path);
}
$this->all_global_functions[$func_name] = $path;
// Check for conflict with const names
if (isset($this->all_global_constants[$func_name])) {
$existing_file = $this->all_global_constants[$func_name];
$this->throw_name_conflict($func_name, 'function', $path, 'const', $existing_file);
}
$this->all_global_names[$func_name] = ['type' => 'function', 'file' => $path];
}
}
// Collect global const names for uniqueness check
if (!empty($metadata['global_const_names'])) {
foreach ($metadata['global_const_names'] as $const_name) {
if (isset($this->all_global_constants[$const_name])) {
// Duplicate const name
$existing_file = $this->all_global_constants[$const_name];
$this->throw_duplicate_global($const_name, 'const', $existing_file, $path);
}
$this->all_global_constants[$const_name] = $path;
// Check for conflict with function names
if (isset($this->all_global_functions[$const_name])) {
$existing_file = $this->all_global_functions[$const_name];
$this->throw_name_conflict($const_name, 'const', $path, 'function', $existing_file);
}
$this->all_global_names[$const_name] = ['type' => 'const', 'file' => $path];
}
}
// Check global functions for @decorator
if (!empty($metadata['global_functions_with_decorators'])) {
foreach ($metadata['global_functions_with_decorators'] as $func_name => $func_data) {
$decorators = $func_data['decorators'] ?? [];
$line = $func_data['line'] ?? 0;
foreach ($decorators as $decorator) {
$decorator_name = $decorator['name'] ?? $decorator[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$func_name] = true;
} else {
// Global function with non-@decorator decorator
$this->throw_global_function_decorator($func_name, $path, $line, $decorator_name);
}
}
}
}
// Check static methods for @decorator
if (!empty($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$method_name] = true;
}
}
}
}
}
// Check instance methods for @decorator
if (!empty($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$method_name] = true;
}
}
}
}
}
}
// Step 2: Validate static and instance method decorators against whitelist
foreach ($files as $path => $metadata) {
// Skip non-JavaScript files
if (($metadata['extension'] ?? '') !== 'js') {
continue;
}
$class_name = $metadata['class'] ?? 'Unknown';
// Check static methods
if (!empty($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name !== 'decorator' && !isset($this->decorator_whitelist[$decorator_name])) {
$this->throw_unwhitelisted_decorator($decorator_name, $class_name, $method_name, $path);
}
}
}
}
}
// Check instance methods
if (!empty($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name !== 'decorator' && !isset($this->decorator_whitelist[$decorator_name])) {
$this->throw_unwhitelisted_decorator($decorator_name, $class_name, $method_name, $path);
}
}
}
}
}
}
}
// Note: Duplicate/conflict checking methods removed
// This functionality is now handled by BundleCompiler::_check_js_naming_conflicts()
// which checks only the files being bundled, not all files in the project
private function throw_global_function_decorator(string $func_name, string $path, int $line, string $decorator_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Global function cannot use decorator\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$path}\n";
$error_message .= "Line: {$line}\n";
$error_message .= "Function: {$func_name}\n";
$error_message .= "Decorator: @{$decorator_name}\n\n";
$error_message .= "Global functions may only use the @decorator marker.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The function '{$func_name}' has decorator '@{$decorator_name}'.\n";
$error_message .= "Only the '@decorator' marker is allowed on global functions.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " @memoize\n";
$error_message .= " function myFunction() { ... }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " @decorator\n";
$error_message .= " function myDecorator(target, key, descriptor) { ... }\n\n";
$error_message .= "WHY THIS RESTRICTION:\n";
$error_message .= "- Global functions are processed differently than class methods\n";
$error_message .= "- The @decorator marker identifies decorator implementations\n";
$error_message .= "- Other decorators can only be used on static class methods\n\n";
$error_message .= "FIX OPTIONS:\n";
$error_message .= "1. Remove the '@{$decorator_name}' decorator\n";
$error_message .= "2. Move the function into a class as a static method\n";
$error_message .= "3. If this IS a decorator implementation, use '@decorator' instead\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
private function throw_unwhitelisted_decorator(string $decorator_name, string $class_name, string $method_name, string $path): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Decorator not whitelisted\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$path}\n";
$error_message .= "Class: {$class_name}\n";
$error_message .= "Method: {$method_name}\n";
$error_message .= "Decorator: @{$decorator_name}\n\n";
$error_message .= "The decorator '@{$decorator_name}' is not whitelisted.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "Only decorators marked with @decorator can be used.\n";
$error_message .= "The '{$decorator_name}' function/method needs @decorator marker.\n\n";
$error_message .= "EXAMPLE OF WHITELISTING:\n";
$error_message .= " // Mark the decorator implementation:\n";
$error_message .= " @decorator\n";
$error_message .= " function {$decorator_name}(target, key, descriptor) {\n";
$error_message .= " // Decorator implementation\n";
$error_message .= " return descriptor;\n";
$error_message .= " }\n\n";
$error_message .= " // Or in a class:\n";
$error_message .= " class Decorators {\n";
$error_message .= " @decorator\n";
$error_message .= " static {$decorator_name}(target, key, descriptor) {\n";
$error_message .= " return descriptor;\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "WHY THIS MATTERS:\n";
$error_message .= "- Decorators must be explicitly whitelisted\n";
$error_message .= "- This prevents typos and undefined decorators\n";
$error_message .= "- Ensures decorators are properly implemented\n\n";
$error_message .= "FIX:\n";
$error_message .= "Add @decorator to the '{$decorator_name}' function/method definition.\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DefensiveCoding_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-DEFENSIVE-01';
}
public function get_name(): string
{
return 'JavaScript Defensive Coding Check';
}
public function get_description(): string
{
return 'Prohibits existence checks - code must fail loudly if dependencies are missing';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for defensive coding violations (from line 833)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Pattern 1: typeof variable checks (!== undefined, === undefined, == 'function', etc.)
// Match: typeof SomeVar !== 'undefined' or typeof SomeVar == 'function'
if (preg_match('/typeof\s+(\w+)\s*([!=]=+)\s*[\'"]?(undefined|function)[\'"]?/i', $line, $matches)) {
$variable = $matches[1];
// Skip if it's a property check (contains dot)
if (!str_contains($variable, '.')) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
}
// Pattern 2: typeof window.variable checks
if (preg_match('/typeof\s+window\.(\w+)\s*([!=]=+)\s*[\'"]?undefined[\'"]?/i', $line, $matches)) {
$variable = 'window.' . $matches[1];
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All global variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
// Pattern 3: if (variable) or if (!variable) existence checks (more careful pattern)
// Only match simple variables, not property access
if (preg_match('/if\s*\(\s*(!)?(\w+)\s*\)/', $line, $matches)) {
$variable = $matches[2];
// Skip if it's a property or array access or a boolean-like variable name
if (!str_contains($line, '.' . $variable) &&
!str_contains($line, '[' . $variable) &&
!str_contains($line, $variable . '.') &&
!str_contains($line, $variable . '[') &&
!preg_match('/^(is|has|can|should|will|did|was)[A-Z]/', $variable) && // Skip boolean-named vars
!in_array(strtolower($variable), ['true', 'false', 'null', 'undefined'])) { // Skip literals
// Check if this looks like an existence check by looking at context
if (preg_match('/if\s*\(\s*(!)?typeof\s+' . preg_quote($variable, '/') . '/i', $line) ||
preg_match('/if\s*\(\s*' . preg_quote($variable, '/') . '\s*&&\s*' . preg_quote($variable, '/') . '\./i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
}
}
// Pattern 4: Guard clauses like: Rsx && Rsx.method()
if (preg_match('/(\w+)\s*&&\s*\1\.\w+/i', $line, $matches)) {
$variable = $matches[1];
// Skip common boolean variable patterns
if (!preg_match('/^(is|has|can|should|will|did|was)[A-Z]/', $variable)) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Guard clause checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the guard clause. Use '{$variable}.method()' directly.",
'high'
);
}
}
// Pattern 5: try/catch used for existence checking (simplified detection)
if (preg_match('/try\s*\{.*?(\w+).*?\}\s*catch/i', $line, $matches)) {
// This is a simplified check - in reality you'd need multi-line parsing
// Skip for now as it's complex to detect intent
}
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DirectAjaxApi_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-AJAX-01';
}
public function get_name(): string
{
return 'Direct AJAX API Call Check';
}
public function get_description(): string
{
return "Detects direct $.ajax calls to /_ajax/ endpoints instead of using JS controller stubs";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript files for direct $.ajax calls to /_ajax/ endpoints
* Instead of:
* await $.ajax({ url: '/_ajax/Controller/action', ... })
* Should use:
* await Controller.action(params)
* Or:
* await Ajax.call('Controller', 'action', params)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip Ajax.js itself
if (str_ends_with($file_path, '/Ajax.js')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ or app/RSpade/
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
// Pattern to match $.ajax({ url: '/_ajax/Controller/action'
// This handles both single-line and multi-line cases
$full_content = implode("\n", $lines);
// Match $.ajax({ with optional whitespace/newlines, then url: with quotes around /_ajax/
// Capture controller and action names for suggestion
$pattern = '/\$\.ajax\s*\(\s*\{[^}]*?url\s*:\s*[\'"](\/_ajax\/([A-Za-z_][A-Za-z0-9_]*)\/([A-Za-z_][A-Za-z0-9_]*))[^\'"]*[\'"]/s';
if (preg_match_all($pattern, $full_content, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$matched_text = $match[0];
$offset = $match[1];
$url = $matches[1][$index][0];
$controller = $matches[2][$index][0];
$action = $matches[3][$index][0];
// Find line number
$line_number = substr_count(substr($full_content, 0, $offset), "\n") + 1;
// Get the actual line for display
$line = $lines[$line_number - 1] ?? '';
// Build suggestion
$suggestion = "Instead of direct $.ajax() call to '{$url}', use:\n";
$suggestion .= " 1. Preferred: await {$controller}.{$action}(params)\n";
$suggestion .= " 2. Alternative: await Ajax.call('{$controller}', '{$action}', params)\n";
$suggestion .= "The JS stub handles session expiry, notifications, and response unwrapping.";
$this->add_violation(
$file_path,
$line_number,
"Direct $.ajax() call to internal API endpoint '{$url}' detected. Use JS controller stub instead.",
trim($line),
$suggestion,
'high'
);
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class DocumentReady_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-READY-01';
}
public function get_name(): string
{
return 'JavaScript Document Ready Check';
}
public function get_description(): string
{
return 'Enforces use of ES6 class lifecycle methods instead of window.onload or jQuery ready';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Whether this rule is called during manifest scan
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* document ready patterns prevent the framework's auto-initialization from functioning correctly.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Process file during manifest update to extract document ready violations
*/
public function on_manifest_file_update(string $file_path, string $contents, array $metadata = []): ?array
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return null;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return null;
}
$lines = explode("\n", $contents);
$violations = [];
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for window.onload patterns
if (preg_match('/\bwindow\s*\.\s*onload\s*=/', $line)) {
$violations[] = [
'type' => 'window_onload',
'line' => $line_number,
'code' => trim($line)
];
break; // Only need first violation
}
// Check for various jQuery ready patterns and DOMContentLoaded
// Patterns: $().ready, $(document).ready, $("document").ready, $('document').ready, $(function(), DOMContentLoaded
$jquery_ready_patterns = [
'/\$\s*\(\s*\)\s*\.\s*ready\s*\(/', // $().ready(
'/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', // $(document).ready( with spaces
'/\$\s*\(\s*["\']document["\']\s*\)\s*\.\s*ready\s*\(/', // $("document").ready( or $('document').ready(
'/\$\s*\(\s*function\s*\(/', // $(function() - shorthand for $(document).ready
'/jQuery\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', // jQuery(document).ready(
'/jQuery\s*\(\s*["\']document["\']\s*\)\s*\.\s*ready\s*\(/', // jQuery("document").ready( or jQuery('document').ready(
'/jQuery\s*\(\s*function\s*\(/', // jQuery(function() - shorthand
'/document\s*\.\s*addEventListener\s*\(\s*["\']DOMContentLoaded[\"\']/', // document.addEventListener("DOMContentLoaded" or 'DOMContentLoaded'
];
foreach ($jquery_ready_patterns as $pattern) {
if (preg_match($pattern, $line)) {
$violations[] = [
'type' => 'jquery_ready',
'line' => $line_number,
'code' => trim($line)
];
break; // Only report once per line
}
}
// Stop after first violation
if (!empty($violations)) {
break;
}
}
if (!empty($violations)) {
return ['document_ready_violations' => $violations];
}
return null;
}
/**
* Check JavaScript file for document ready violations stored in metadata
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Check for violations in code quality metadata
if (isset($metadata['code_quality_metadata']['JS-READY-01']['document_ready_violations'])) {
$violations = $metadata['code_quality_metadata']['JS-READY-01']['document_ready_violations'];
// Get appropriate suggestion based on code location
$suggestion = InitializationSuggestions::get_suggestion($file_path);
// Throw on first violation
foreach ($violations as $violation) {
$type = $violation['type'];
$line = $violation['line'];
$code = $violation['code'];
if ($type === 'window_onload') {
$error_message = "Code Quality Violation (JS-READY-01) - Prohibited Window Onload Pattern\n\n";
$error_message .= "window.onload is not allowed. Use ES6 class with lifecycle methods instead.\n\n";
} else {
$error_message = "Code Quality Violation (JS-READY-01) - Prohibited jQuery Ready Pattern\n\n";
$error_message .= "jQuery ready/DOMContentLoaded patterns are not allowed. Use ES6 class with lifecycle methods instead.\n\n";
}
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line}\n";
$error_message .= "Code: {$code}\n\n";
$error_message .= $suggestion;
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($file_path),
$line
);
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DomMethod_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-DOM-01';
}
public function get_name(): string
{
return 'JavaScript DOM Method Usage Check';
}
public function get_description(): string
{
return 'Enforces jQuery instead of native DOM methods';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Check for document.getElementById
if (preg_match('/\bdocument\.getElementById\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementById(id)' with '$('#' + id)' or use a jQuery selector directly like $('#myId'). " .
"jQuery provides a more consistent and powerful API for DOM manipulation that works across all browsers.",
'medium'
);
}
// Check for document.createElement
if (preg_match('/\bdocument\.createElement\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.createElement(tagName)' with '$('<' + tagName + '>')' or use jQuery element creation like $('<div>'). " .
"jQuery provides a more fluent API for creating and manipulating DOM elements.",
'medium'
);
}
// Check for document.getElementsByClassName
if (preg_match('/\bdocument\.getElementsByClassName\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementsByClassName(className)' with $('.' + className) or use a jQuery class selector directly like $('.myClass'). " .
"jQuery provides a more consistent API that returns a jQuery object with many useful methods.",
'medium'
);
}
// Check for document.getElementsByTagName
if (preg_match('/\bdocument\.getElementsByTagName\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementsByTagName(tagName)' with $(tagName) or use a jQuery tag selector like $('div'). " .
"jQuery provides a unified API for element selection.",
'medium'
);
}
// Check for document.querySelector
if (preg_match('/\bdocument\.querySelector\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.querySelector(selector)' with $(selector). " .
"jQuery's selector engine is more powerful and consistent across browsers.",
'medium'
);
}
// Check for document.querySelectorAll
if (preg_match('/\bdocument\.querySelectorAll\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.querySelectorAll(selector)' with $(selector). " .
"jQuery automatically handles collections and provides chainable methods.",
'medium'
);
}
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class FrameworkInitialization_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-INIT-FW-01';
}
public function get_name(): string
{
return 'Framework Code Initialization Pattern Check';
}
public function get_description(): string
{
return 'Enforces proper initialization patterns for framework JavaScript code in /app/RSpade directory';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check JavaScript file for proper framework initialization patterns
* Framework code in /app/RSpade should use _on_framework_* methods
* User methods (on_modules_*, on_app_*) are forbidden in framework code
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in /app/RSpade directory
if (!str_contains($file_path, '/app/RSpade/')) {
return;
}
// Check if it's in an allowed subdirectory
if (!$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Get original content for pattern detection
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($original_lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for user code methods (forbidden in framework code)
$user_methods = [
'on_modules_define',
'on_modules_init',
'on_app_define',
'on_app_init',
'on_app_ready'
];
foreach ($user_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"User initialization method '{$method}' cannot be used in framework code.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'critical'
);
}
}
// Check for Rsx.on('ready') pattern
if (preg_match('/\bRsx\s*\.\s*on\s*\(\s*[\'\"]ready[\'\"]/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Rsx.on('ready') is deprecated. Use framework lifecycle methods instead.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'high'
);
}
// Check for jQuery ready patterns (should not be in framework code)
if (preg_match('/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line) ||
preg_match('/\$\s*\(\s*function\s*\(/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"jQuery ready patterns are not allowed in framework code. Use framework lifecycle methods.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'high'
);
}
// Validate correct framework method usage (informational)
$framework_methods = [
'_on_framework_core_define',
'_on_framework_core_init',
'_on_framework_module_define',
'_on_framework_module_init'
];
foreach ($framework_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
// This is correct usage - no violation
// Could log this for validation purposes if needed
}
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class InitializationPattern_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-INIT-USER-01';
}
public function get_name(): string
{
return 'User Code Initialization Pattern Check';
}
public function get_description(): string
{
return 'Enforces proper initialization patterns for user JavaScript code in /rsx directory';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for proper initialization patterns
* User code in /rsx should use on_modules_* or on_app_* methods
* Framework methods (_on_framework_*) are forbidden in user code
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in /rsx directory
if (!str_contains($file_path, '/rsx/')) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Get original content for pattern detection
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($original_lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for Rsx.on('ready') pattern
if (preg_match('/\bRsx\s*\.\s*on\s*\(\s*[\'"]ready[\'"]/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Rsx.on('ready') is deprecated. Use ES6 class lifecycle methods instead.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'high'
);
}
// Check for framework methods (forbidden in user code)
$framework_methods = [
'_on_framework_core_define',
'_on_framework_core_init',
'_on_framework_module_define',
'_on_framework_module_init'
];
foreach ($framework_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Framework initialization method '{$method}' cannot be used in user code.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'critical'
);
}
}
// Check for jQuery ready patterns (handled by DocumentReadyRule but add context here)
if (preg_match('/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line) ||
preg_match('/\$\s*\(\s*function\s*\(/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"jQuery ready patterns are not allowed. Use ES6 class lifecycle methods.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'high'
);
}
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryLengthCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-LENGTH-01';
}
public function get_name(): string
{
return 'jQuery .length Existence Check';
}
public function get_description(): string
{
return 'Enforces use of .exists() instead of .length for jQuery existence checks';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
// Patterns to detect:
// if($(selector).length)
// if(!$(selector).length)
// if($variable.length)
// if(!$variable.length)
// Also within compound conditions
// Check if line contains 'if' and '.length'
if (str_contains($sanitized_line, 'if') && str_contains($sanitized_line, '.length')) {
// Multiple patterns to check
$patterns = [
// Direct jQuery selector patterns
'/if\s*\(\s*!\s*\$\s*\([^)]+\)\.length/', // if(!$(selector).length
'/if\s*\(\s*\$\s*\([^)]+\)\.length/', // if($(selector).length
// jQuery variable patterns
'/if\s*\(\s*!\s*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // if(!$variable.length
'/if\s*\(\s*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // if($variable.length
// Within compound conditions (with && or ||)
'/if\s*\([^)]*[&|]{2}[^)]*\$\s*\([^)]+\)\.length/', // compound with $(selector).length
'/if\s*\([^)]*\$\s*\([^)]+\)\.length[^)]*[&|]{2}/', // compound with $(selector).length
'/if\s*\([^)]*[&|]{2}[^)]*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // compound with $variable.length
'/if\s*\([^)]*\$[a-zA-Z_][a-zA-Z0-9_]*\.length[^)]*[&|]{2}/', // compound with $variable.length
];
$found = false;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $sanitized_line)) {
$found = true;
break;
}
}
if ($found) {
$original_line = $original_lines[$line_num] ?? $sanitized_line;
$this->add_violation(
$file_path,
$line_number,
"Use .exists() instead of .length for jQuery existence checks.",
trim($original_line),
"Replace .length with .exists() for checking jQuery element existence. " .
"For example: use '$(selector).exists()' instead of '$(selector).length', " .
"or '\$variable.exists()' instead of '\$variable.length'. " .
"The .exists() method is more semantic and clearly indicates the intent of checking for element presence.",
'medium'
);
}
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQuerySubmitUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-SUBMIT-01';
}
public function get_name(): string
{
return 'jQuery .submit() Usage Check';
}
public function get_description(): string
{
return "Detects deprecated jQuery .submit() usage and recommends .trigger('submit') or .requestSubmit()";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'low';
}
/**
* Check JavaScript file for .submit() usage
* Recommends .trigger('submit') or .requestSubmit() instead
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Remove comments and strings to avoid false positives
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized = $sanitized_data['content'];
// Pattern matches:
// $variable.submit()
// $(selector).submit()
// that.$anything.submit()
// this.$anything.submit()
$pattern = '/(\$[a-zA-Z_][a-zA-Z0-9_]*|(?:this|that)\.\$[a-zA-Z0-9_.]+|\$\([^)]+\))\.submit\s*\(/';
preg_match_all($pattern, $sanitized, $matches, PREG_OFFSET_CAPTURE);
if (empty($matches[0])) {
return;
}
$lines = explode("\n", $contents);
foreach ($matches[0] as $match) {
$offset = $match[1];
$matched_text = $match[0];
// Find line number from offset
$line_number = 1;
$current_offset = 0;
foreach ($lines as $index => $line) {
$line_length = strlen($line) + 1; // +1 for newline
if ($current_offset + $line_length > $offset) {
$line_number = $index + 1;
break;
}
$current_offset += $line_length;
}
$code_snippet = trim($lines[$line_number - 1] ?? '');
$this->add_violation(
$file_path,
$line_number,
"Use .trigger('submit') or .requestSubmit() instead of deprecated .submit()",
$code_snippet,
"Replace .submit() with .trigger('submit') for event triggering or .requestSubmit() for actual form submission with validation",
'low'
);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-01';
}
public function get_name(): string
{
return 'JavaScript jQuery Usage Check';
}
public function get_description(): string
{
return "Enforces use of '$' shorthand instead of 'jQuery' for consistency";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'low';
}
/**
* Check JavaScript file for 'jQuery' usage instead of '$' (from line 1307)
* Enforces use of '$' shorthand for consistency
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for 'jQuery.' or 'jQuery(' usage
if (preg_match('/\bjQuery\s*[\.\(]/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Use '$' instead of 'jQuery' for consistency and brevity.",
trim($line),
"Replace 'jQuery' with '$'.",
'low'
);
}
}
}
}

View File

@@ -0,0 +1,226 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-VAR-01';
}
public function get_name(): string
{
return 'jQuery Variable Naming Convention';
}
public function get_description(): string
{
return 'Enforces $ prefix for variables storing jQuery objects';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* jQuery methods that return jQuery objects
*/
private const JQUERY_OBJECT_METHODS = [
'parent', 'parents', 'parentsUntil', 'closest',
'find', 'children', 'contents',
'next', 'nextAll', 'nextUntil',
'prev', 'prevAll', 'prevUntil',
'siblings', 'add', 'addBack', 'andSelf',
'end', 'filter', 'not', 'has',
'eq', 'first', 'last', 'slice',
'map', 'clone', 'wrap', 'wrapAll', 'wrapInner',
'unwrap', 'replaceWith', 'replaceAll',
'prepend', 'append', 'prependTo', 'appendTo',
'before', 'after', 'insertBefore', 'insertAfter',
'detach', 'empty', 'remove'
];
/**
* jQuery methods that return scalar values (not jQuery objects)
*/
private const SCALAR_METHODS = [
'data', 'attr', 'val', 'text', 'html', 'prop', 'css',
'offset', 'position', 'scrollTop', 'scrollLeft',
'width', 'height', 'innerWidth', 'innerHeight',
'outerWidth', 'outerHeight',
'index', 'size', 'length', 'get', 'toArray',
'serialize', 'serializeArray',
'is', 'hasClass', 'is_visible' // Custom RSpade methods
];
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Pattern to match variable assignments
// Captures: 1=var declaration, 2=variable name, 3=right side expression
$pattern = '/(?:^|\s)((?:let\s+|const\s+|var\s+)?)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+?)(?:;|$)/';
if (preg_match($pattern, $sanitized_line, $matches)) {
$var_decl = $matches[1];
$var_name = $matches[2];
$right_side = trim($matches[3]);
$has_dollar = str_starts_with($var_name, '$');
// Analyze the right side to determine if it returns jQuery object or scalar
$expected_type = $this->analyze_expression($right_side);
if ($expected_type === 'jquery') {
// Should have $ prefix
if (!$has_dollar) {
$this->add_violation(
$file_path,
$line_number,
"jQuery object must be stored in variable starting with $.",
trim($original_line),
"Rename variable '{$var_name}' to '\${$var_name}'. " .
"The expression returns a jQuery object and must be stored in a variable with $ prefix. " .
"In RSpade, $ prefix indicates jQuery objects only.",
'medium'
);
}
} elseif ($expected_type === 'scalar') {
// Should NOT have $ prefix
if ($has_dollar) {
$this->add_violation(
$file_path,
$line_number,
"Scalar values should not use $ prefix.",
trim($original_line),
"Remove $ prefix from variable '{$var_name}'. Rename to '" . substr($var_name, 1) . "'. " .
"The expression returns a scalar value (string, number, boolean, or DOM element), not a jQuery object. " .
"In RSpade, $ prefix is reserved for jQuery objects only.",
'medium'
);
}
}
// If expected_type is 'unknown', we don't enforce either way
}
}
}
/**
* Analyze an expression to determine if it returns jQuery object or scalar
* @return string 'jquery', 'scalar', or 'unknown'
*/
private function analyze_expression(string $expr): string
{
$expr = trim($expr);
// Direct jQuery selector: $(...)
if (preg_match('/^\$\s*\(/', $expr)) {
// Check if followed by method chain
if (preg_match('/^\$\s*\([^)]*\)(.*)/', $expr, $matches)) {
$chain = trim($matches[1]);
if ($chain === '') {
return 'jquery'; // Just $(...) with no methods
}
return $this->analyze_method_chain($chain);
}
return 'jquery';
}
// Variable starting with $ (assumed to be jQuery)
if (preg_match('/^\$[a-zA-Z_][a-zA-Z0-9_]*(.*)/', $expr, $matches)) {
$chain = trim($matches[1]);
if ($chain === '') {
return 'jquery'; // Just $variable with no methods
}
if (str_starts_with($chain, '[')) {
// Array access like $element[0]
return 'scalar';
}
return $this->analyze_method_chain($chain);
}
// Everything else is unknown or definitely not jQuery
return 'unknown';
}
/**
* Analyze a method chain to determine final return type
* @param string $chain The method chain starting with . or [
* @return string 'jquery', 'scalar', or 'unknown'
*/
private function analyze_method_chain(string $chain): string
{
if (empty($chain)) {
return 'jquery'; // No methods means original jQuery object
}
// Array access [0] or [index] returns DOM element (scalar)
if (preg_match('/^\[[\d]+\]/', $chain)) {
return 'scalar';
}
// Find the last method call in the chain
// Match patterns like .method() or .method(args)
$methods = [];
preg_match_all('/\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)/', $chain, $methods);
if (empty($methods[1])) {
// No method calls found
return 'unknown';
}
// Check the last method to determine return type
$last_method = end($methods[1]);
if (in_array($last_method, self::JQUERY_OBJECT_METHODS, true)) {
return 'jquery';
}
if (in_array($last_method, self::SCALAR_METHODS, true)) {
return 'scalar';
}
// Unknown method - could be custom plugin
return 'unknown';
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JQueryVisibilityCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-VISIBLE-01';
}
public function get_name(): string
{
return 'jQuery .is(\':visible\') Check';
}
public function get_description(): string
{
return 'Enforces use of .is_visible() instead of .is(\':visible\') for jQuery visibility checks';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get original content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Pattern to match .is(':visible') or .is(":visible")
$pattern = '/\.is\s*\(\s*[\'\"]:visible[\'\"]\s*\)/';
foreach ($original_lines as $line_num => $original_line) {
$line_number = $line_num + 1;
// Skip empty lines
if (trim($original_line) === '') {
continue;
}
// Check if line contains .is(':visible') pattern
if (preg_match($pattern, $original_line, $matches, PREG_OFFSET_CAPTURE)) {
$match_position = $matches[0][1];
// Check if there's a // comment before the match
$comment_pos = strpos($original_line, '//');
if ($comment_pos !== false && $comment_pos < $match_position) {
// The match is in a comment, skip it
continue;
}
// Check if the match is inside a /* */ comment block (simplified check)
// This is a basic check - for full accuracy would need to track multi-line comments
$before_match = substr($original_line, 0, $match_position);
if (str_contains($before_match, '/*') && !str_contains($before_match, '*/')) {
// Likely inside a block comment
continue;
}
$this->add_violation(
$file_path,
$line_number,
"Use .is_visible() instead of .is(':visible') for jQuery visibility checks.",
trim($original_line),
"Replace .is(':visible') with .is_visible() for checking jQuery element visibility. " .
"IMPORTANT: .is_visible() is a custom jQuery extension implemented in the RSpade framework " .
"and is available throughout the codebase. It provides better performance and clearer intent " .
"than the :visible selector. Example: use '$(selector).is_visible()' instead of '$(selector).is(':visible')'.",
'medium'
);
}
}
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
use App\RSpade\Core\Cache\RsxCache;
/**
* Check Jqhtml_Component implementations for common AI agent mistakes
* Validates that components follow correct patterns
*/
class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-IMPL-01';
}
public function get_name(): string
{
return 'Jqhtml Component Implementation Check';
}
public function get_description(): string
{
return 'Validates Jqhtml_Component subclasses follow correct patterns';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
/**
* Run during manifest build for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a JavaScript file
if (!isset($metadata['extension']) || $metadata['extension'] !== 'js') {
return;
}
// Skip if not a Jqhtml_Component subclass
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Jqhtml_Component') {
return;
}
// Check cache to avoid redundant validation
$cache_key = $metadata['hash'] ?? md5($contents);
if (RsxCache::get_persistent($cache_key, false) === true) {
// Already validated
return;
}
$lines = explode("\n", $contents);
// Check for render() method and incorrect lifecycle methods
foreach ($lines as $line_num => $line) {
$trimmed = trim($line);
// Check for render() method
if (preg_match('/^render\s*\(/', $trimmed)) {
$this->throw_render_method_error($file_path, $line_num + 1, $metadata['class'] ?? 'Unknown');
}
// Check for incorrect event method names (create, load, ready without on_ prefix)
if (preg_match('/^(create|load|ready)\s*\(/', $trimmed, $matches)) {
$method = $matches[1];
$this->throw_lifecycle_method_error($file_path, $line_num + 1, $method);
}
}
// Mark as validated in cache
RsxCache::set_persistent($cache_key, true);
}
private function throw_render_method_error(string $file_path, int $line_number, string $class_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Jqhtml component should not have render() method\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line_number}\n";
$error_message .= "Class: {$class_name}\n\n";
$error_message .= "Jqhtml components should not define a render() method.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The render() method is not part of the Jqhtml_Component lifecycle.\n";
$error_message .= "Jqhtml components use template files (.jqhtml) for rendering.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " render() {\n";
$error_message .= " return '<div>...</div>';\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " // Create a template file: my_component.jqhtml\n";
$error_message .= " <div>\n";
$error_message .= " <%= content() %>\n";
$error_message .= " </div>\n\n";
$error_message .= " // JavaScript class handles logic:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " on_ready() {\n";
$error_message .= " // Component logic here\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "WHY THIS MATTERS:\n";
$error_message .= "- Jqhtml separates template from logic\n";
$error_message .= "- Templates are pre-compiled for performance\n";
$error_message .= "- The render() pattern is from React, not Jqhtml\n\n";
$error_message .= "FIX:\n";
$error_message .= "1. Remove the render() method\n";
$error_message .= "2. Create a .jqhtml template file for the component\n";
$error_message .= "3. Use lifecycle methods like on_create(), on_load(), on_ready()\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
private function throw_lifecycle_method_error(string $file_path, int $line_number, string $method_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Jqhtml lifecycle method missing 'on_' prefix\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line_number}\n";
$error_message .= "Method: {$method_name}()\n\n";
$error_message .= "Jqhtml lifecycle methods must use the 'on_' prefix.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The method '{$method_name}()' should be 'on_{$method_name}()'.\n";
$error_message .= "Jqhtml components use specific lifecycle method names.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " create() { ... } // Wrong\n";
$error_message .= " load() { ... } // Wrong\n";
$error_message .= " ready() { ... } // Wrong\n";
$error_message .= " }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " on_create() { ... } // Correct\n";
$error_message .= " on_load() { ... } // Correct\n";
$error_message .= " on_ready() { ... } // Correct\n";
$error_message .= " }\n\n";
$error_message .= "LIFECYCLE METHODS:\n";
$error_message .= "- on_create(): Called when component is created\n";
$error_message .= "- on_load(): Called to load async data\n";
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
$error_message .= "- on_destroy(): Called when component is destroyed\n\n";
$error_message .= "FIX:\n";
$error_message .= "Rename '{$method_name}()' to 'on_{$method_name}()'\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JqhtmlDataInCreate_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQHTML-01';
}
public function get_name(): string
{
return 'Jqhtml Component this.data in on_create() Check';
}
public function get_description(): string
{
return 'Ensures this.data is not used in on_create() method of Jqhtml components';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for improper this.data usage in on_create() methods of Jqhtml components
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get violations from AST parser (with caching)
$violations = $this->parse_with_acorn($file_path);
if (empty($violations)) {
return;
}
// Process violations
foreach ($violations as $violation) {
$line_number = $violation['line'];
$class_name = $violation['className'] ?? 'unknown';
$code_snippet = $violation['codeSnippet'] ?? 'this.data';
$this->add_violation(
$file_path,
$line_number,
"Jqhtml Component Error: 'this.data' used in on_create() method of class '{$class_name}'. " .
"The 'this.data' property is only available during on_load() and later lifecycle steps. " .
"It is used to store data fetched from AJAX or other async operations.",
$code_snippet,
"Use 'this.args' instead to access the parameters passed to the component at creation time. " .
"The args contain attributes from the component's invocation in templates or JavaScript. " .
"Example: Change 'this.data.initial_value' to 'this.args.initial_value'.",
'high'
);
}
}
/**
* Parse JavaScript file with acorn AST parser
* Results are cached based on file modification time
*/
protected function parse_with_acorn(string $file_path): array
{
// Create cache directory if needed
$cache_dir = storage_path('rsx-tmp/persistent/code-quality-jqhtml-data');
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0777, true);
}
// Generate cache key based on file path and mtime
$file_mtime = filemtime($file_path);
$file_size = filesize($file_path);
$cache_key = md5($file_path . ':jqhtml-data');
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Check if cached result exists and is valid
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true);
if ($cache_data &&
isset($cache_data['mtime']) && $cache_data['mtime'] == $file_mtime &&
isset($cache_data['size']) && $cache_data['size'] == $file_size) {
// Cache is valid
return $cache_data['violations'] ?? [];
}
}
// Create parser script if it doesn't exist
$parser_script = storage_path('rsx-tmp/persistent/parse-jqhtml-data.js');
if (!file_exists($parser_script)) {
$this->create_parser_script($parser_script);
}
// Run parser
$command = sprintf(
'node %s %s 2>&1',
escapeshellarg($parser_script),
escapeshellarg($file_path)
);
$output = shell_exec($command);
if (!$output) {
return [];
}
$result = json_decode($output, true);
if (!$result || !isset($result['violations'])) {
// Parser error - don't cache
return [];
}
// Cache the result
$cache_data = [
'mtime' => $file_mtime,
'size' => $file_size,
'violations' => $result['violations']
];
file_put_contents($cache_file, json_encode($cache_data));
return $result['violations'];
}
/**
* Create the Node.js parser script
*/
protected function create_parser_script(string $script_path): void
{
$dir = dirname($script_path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$script_content = <<<'JAVASCRIPT'
#!/usr/bin/env node
const fs = require('fs');
const acorn = require('acorn');
const walk = require('acorn-walk');
// Classes that are Jqhtml components
const JQHTML_COMPONENTS = new Set([
'Jqhtml_Component', '_Base_Jqhtml_Component', 'Component'
]);
function analyzeFile(filePath) {
const code = fs.readFileSync(filePath, 'utf8');
const lines = code.split('\n');
let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
locations: true
});
} catch (e) {
// Parse error - return empty violations
console.log(JSON.stringify({ violations: [] }));
return;
}
const violations = [];
let currentClass = null;
let inOnCreate = false;
// Helper to check if a class extends Jqhtml_Component
function isJqhtmlComponent(extendsClass) {
if (!extendsClass) return false;
return JQHTML_COMPONENTS.has(extendsClass) ||
extendsClass.includes('Component') ||
extendsClass.includes('Jqhtml');
}
// Walk the AST
walk.simple(ast, {
ClassDeclaration(node) {
currentClass = {
name: node.id.name,
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
ClassExpression(node) {
currentClass = {
name: node.id?.name || 'anonymous',
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
MethodDefinition(node) {
// Check if this is on_create method
if (node.key.name === 'on_create' && currentClass?.isJqhtml) {
inOnCreate = true;
// Walk the method body looking for this.data
walk.simple(node.value.body, {
MemberExpression(memberNode) {
// Check for this.data pattern
if (memberNode.object.type === 'ThisExpression' &&
memberNode.property.name === 'data') {
// Found this.data in on_create
const lineContent = lines[memberNode.loc.start.line - 1] || '';
violations.push({
line: memberNode.loc.start.line,
column: memberNode.loc.start.column,
className: currentClass.name,
codeSnippet: lineContent.trim()
});
}
}
});
inOnCreate = false;
}
}
});
console.log(JSON.stringify({ violations }));
}
// Main
if (process.argv.length < 3) {
console.error('Usage: node parse-jqhtml-data.js <file-path>');
process.exit(1);
}
try {
analyzeFile(process.argv[2]);
} catch (e) {
console.error('Error:', e.message);
console.log(JSON.stringify({ violations: [] }));
}
JAVASCRIPT;
file_put_contents($script_path, $script_content);
chmod($script_path, 0755);
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
class JqhtmlOnLoadData_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-LOAD-02';
}
public function get_name(): string
{
return 'JQHTML on_load Data Assignment Check';
}
public function get_description(): string
{
return 'on_load() method should only set this.data properties, not other this. properties';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check that on_load methods only set this.data properties
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// Get JavaScript class from manifest metadata
$js_classes = [];
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
$js_classes = [$metadata['class']];
}
// If no classes in metadata, try to extract from source
if (empty($js_classes)) {
return;
}
// Check each class to see if it's a JQHTML component
foreach ($js_classes as $class_name) {
// Use Manifest to check inheritance (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Look for on_load method
if (!preg_match('/(?:async\s+)?on_load\s*\([^)]*\)\s*\{/i', $class_content, $method_match, PREG_OFFSET_CAPTURE)) {
continue; // No on_load method
}
$method_start = $method_match[0][1];
// Extract the on_load method body
$method_pos = $class_start + $method_start;
$method_brace_count = 0;
$in_method = false;
$method_content = '';
$pos = $method_pos;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$method_brace_count++;
$in_method = true;
} elseif ($char === '}') {
$method_brace_count--;
if ($method_brace_count === 0 && $in_method) {
$method_content = substr($content, $method_pos, $pos - $method_pos + 1);
break;
}
}
$pos++;
}
if (empty($method_content)) {
continue;
}
// Check for this. property assignments
$lines = explode("\n", $method_content);
$line_offset = substr_count(substr($content, 0, $method_pos), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for this.property = value patterns
// Match: this.something =
// But not: this.data.something =
if (preg_match('/\bthis\.(\w+)\s*=/', $line, $matches)) {
$property_name = $matches[1];
// Allow this.data assignments
if ($property_name === 'data') {
continue;
}
// Check if it's a sub-property of data (this.data.something)
if (preg_match('/\bthis\.data\.\w+/', $line)) {
continue;
}
// Check for destructuring into this.data
if (preg_match('/\bthis\.data\s*=\s*\{.*' . preg_quote($property_name, '/') . '/', $line)) {
continue;
}
$this->add_violation(
$file_path,
$actual_line_number,
"Setting 'this.{$property_name}' in on_load() method of class '{$class_name}'. The on_load() method should only update this.data properties.",
trim($line),
"Change to 'this.data.{$property_name} = ...' or move to on_create() if it's component state.",
'high'
);
}
// Also check for Object.assign or similar patterns that set this properties
if (preg_match('/Object\.assign\s*\(\s*this\s*(?!\.data)/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Using Object.assign on 'this' in on_load() method of class '{$class_name}'. The on_load() method should only update this.data.",
trim($line),
"Use 'Object.assign(this.data, ...)' instead, or move to on_create() for component state.",
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
class JqhtmlOnLoadDom_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-LOAD-01';
}
public function get_name(): string
{
return 'JQHTML on_load DOM Access Check';
}
public function get_description(): string
{
return 'on_load() method must not access DOM or call render() - only update this.data';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for DOM access in on_load methods of Jqhtml_Component subclasses
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// Get JavaScript class from manifest metadata
$js_classes = [];
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
$js_classes = [$metadata['class']];
}
// If no classes in metadata, nothing to check
if (empty($js_classes)) {
return;
}
// Check each class to see if it's a JQHTML component
foreach ($js_classes as $class_name) {
// Use Manifest to check inheritance (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
// Not checking inheritance - need PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Look for on_load method
if (!preg_match('/(?:async\s+)?on_load\s*\([^)]*\)\s*\{/i', $class_content, $method_match, PREG_OFFSET_CAPTURE)) {
continue; // No on_load method
}
$method_start = $method_match[0][1];
// Extract the on_load method body
$method_pos = $class_start + $method_start;
$method_brace_count = 0;
$in_method = false;
$method_content = '';
$pos = $method_pos;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$method_brace_count++;
$in_method = true;
} elseif ($char === '}') {
$method_brace_count--;
if ($method_brace_count === 0 && $in_method) {
$method_content = substr($content, $method_pos, $pos - $method_pos + 1);
break;
}
}
$pos++;
}
if (empty($method_content)) {
continue;
}
// Check for DOM access patterns
$lines = explode("\n", $method_content);
$line_offset = substr_count(substr($content, 0, $method_pos), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for this.$ (jQuery element access)
// Match this.$ followed by anything except .ajax/.get/.post/.getJSON
if (preg_match('/\bthis\.\$/', $line)) {
// Check if it's this.$.something (DOM manipulation) vs $.ajax usage
if (preg_match('/\bthis\.\$\./', $line)) {
// this.$. pattern - this is DOM manipulation
$this->add_violation(
$file_path,
$actual_line_number,
"DOM access 'this.\$' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties.',
'high'
);
}
}
// Check for this.$id() calls
if (preg_match('/\bthis\.\$id\s*\(/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"DOM access 'this.\$id()' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties.',
'high'
);
}
// Check for jQuery selector usage (but allow $.ajax)
if (preg_match('/\$\s*\([\'"`]/', $line) && !preg_match('/\$\.(ajax|get|post|getJSON)/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"jQuery selector '\$()' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties. Use $.ajax() for data fetching.',
'high'
);
}
// Check for this.render() calls
if (preg_match('/\bthis\.render\s*\(/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Calling 'this.render()' in on_load() method of class '{$class_name}' is not allowed. Re-render happens automatically after on_load() if this.data changed.",
trim($line),
'Remove this.render() call. The framework automatically re-renders after on_load() if this.data was modified.',
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
class JqhtmlRenderOverride_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-RENDER-01';
}
public function get_name(): string
{
return 'JQHTML render() Method Override Check';
}
public function get_description(): string
{
return 'Components must not override render() method - use on_render(), on_create(), or on_ready() instead';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for render() method override in Jqhtml_Component subclasses
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// Get JavaScript class from manifest metadata
$js_classes = [];
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
$js_classes = [$metadata['class']];
}
// If no classes in metadata, nothing to check
if (empty($js_classes)) {
return;
}
// Check each class to see if it's a JQHTML component
foreach ($js_classes as $class_name) {
// Use Manifest to check inheritance (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
// Not checking inheritance - need PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Check for render() method definition
$lines = explode("\n", $class_content);
$line_offset = substr_count(substr($content, 0, $class_start), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for render() method definition
// Match: render() { or async render() {
// But not: on_render() or other_render() or renderSomething()
if (preg_match('/^(?:async\s+)?render\s*\([^)]*\)\s*\{/', $trimmed)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Component class '{$class_name}' overrides render() method. This is not allowed in JQHTML components.",
trim($line),
'Use lifecycle methods instead: on_render() for initial render setup, on_create() for post-render initialization, or on_ready() for final setup. The render() method is reserved for the framework to render templates.',
'high'
);
}
// Also check for static render method (which would also be wrong)
if (preg_match('/^static\s+(?:async\s+)?render\s*\([^)]*\)\s*\{/', $trimmed)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Component class '{$class_name}' defines a static render() method. This is not allowed in JQHTML components.",
trim($line),
'Use lifecycle methods instead: on_render() for initial render setup, on_create() for post-render initialization, or on_ready() for final setup.',
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JsFallbackLegacy_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-FALLBACK-01';
}
public function get_name(): string
{
return 'JavaScript Fallback/Legacy Code Check';
}
public function get_description(): string
{
return 'Enforces fail-loud principle - no fallback implementations allowed';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check JavaScript file for fallback/legacy code in comments and function calls (from line 1415)
* Enforces fail-loud principle - no fallback implementations allowed
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Use original content to check comments before sanitization
$original_content = file_get_contents($file_path);
$lines = explode("\n", $original_content);
// Also get sanitized content to check for function calls
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for fallback or legacy in comments (case insensitive, with word boundaries)
// But allow fallback* or legacy* as marked exceptions
if (preg_match('/\/\/.*\b(fallback|legacy)\b(?!\*)/i', $line) ||
preg_match('/\/\*.*\b(fallback|legacy)\b(?!\*)/i', $line) ||
preg_match('/\*.*\b(fallback|legacy)\b(?!\*)/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"CRITICAL: Fallback/legacy code detected in comment. This violates the fail-loud principle - there must be ONE way to do things.",
trim($line),
"REMOVE the fallback/legacy code immediately. If primary code fails, it MUST throw an exception, NOT execute alternative code. Fallbacks create non-deterministic behavior and debugging nightmares. Only ONE implementation should exist. If you're an AI/LLM agent: DO NOT decide to keep fallback code - you MUST ask the developer for explicit approval. To mark legitimate uses (rare), add '*' after the word. Remember: Predictable failures are better than unpredictable 'working' code.",
'critical'
);
}
// Check for dual implementation patterns in single-line comments
// These patterns suggest old/new dual implementations which violate the no-fallback rule
if (preg_match('/\/\/.*\bold\W+.*\bnew\b/i', $line) || // "old" ... "new"
preg_match('/\/\/.*\bprevious\W+.*\bnew\b/i', $line) || // "previous" ... "new"
preg_match('/\/\/.*\bbackwards?\s+compatability/i', $line)) { // "backward(s) compatibility" (common misspelling)
$this->add_violation(
$file_path,
$line_number,
"🚨 CRITICAL DUAL IMPLEMENTATION VIOLATION: Comment suggests old/new or backwards compatibility patterns\n\nThis violates the fundamental architecture principle: There is NO 'old' and 'new' way - there is ONLY THE WAY.\nDual implementations cause:\n1. CONFUSION about which code path executes\n2. DUPLICATE CODE that diverges over time\n3. DEBUGGING NIGHTMARES when behavior differs\n4. MAINTENANCE BURDEN keeping both paths in sync",
trim($line),
"IMMEDIATE ACTION REQUIRED:\n1. IDENTIFY what 'old' vs 'new' refers to\n2. REMOVE the old implementation entirely\n3. UPDATE all references to use the single implementation\n4. DO NOT maintain backwards compatibility\n\nFor AI/LLM agents: This is a CRITICAL violation. You MUST:\n- Search for BOTH implementations referenced in the comment\n- Report findings to user before making changes\n- Remove the OLD implementation completely\n- Update all code to use the single 'new' (current) way\n- NEVER maintain dual implementations for compatibility\n\nRemember: There is only ONE way to do things. What's currently called 'new' IS the way.",
'critical'
);
}
// Check for function calls starting or ending with 'fallback'
// This catches: fallback(), fallback_loader(), document_loader_fallback(), etc.
// But NOT: document_fallback_loader() (fallback in middle)
// Use sanitized line to check if there's actual code, but check pattern on original line
if (isset($sanitized_lines[$line_num]) && trim($sanitized_lines[$line_num]) !== '') {
// There's non-comment code on this line
// Pattern matches functions that start with "fallback" OR end with "fallback"
if (preg_match('/\b(fallback\w*|\w+fallback)\s*\(/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"CRITICAL: Fallback function call detected. This violates the fail-loud principle - there must be ONE way to do things.",
trim($line),
"REMOVE the fallback function immediately or RENAME it if it's legitimate required program flow (not an alternative implementation). If primary code fails, it MUST throw an exception, NOT execute alternative code. Fallbacks create non-deterministic behavior and debugging nightmares. Only ONE implementation should exist.",
'critical'
);
}
}
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JsLegacyFunction_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-LEGACY-FUNC-01';
}
public function get_name(): string
{
return 'JavaScript Legacy Function Comment Check';
}
public function get_description(): string
{
return 'Prohibits functions with "legacy" in block comments - enforces no backwards compatibility principle';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check for block comments containing "legacy" directly before function definitions
* Enforces RSX principle of no backwards compatibility functions
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip node_modules directories
if (str_contains($file_path, '/node_modules/')) {
return;
}
// Skip vendor directories
if (str_contains($file_path, '/vendor/')) {
return;
}
$lines = explode("\n", $contents);
$in_block_comment = false;
$block_comment_content = '';
$block_comment_start_line = 0;
for ($i = 0; $i < count($lines); $i++) {
$line = $lines[$i];
$line_number = $i + 1;
$trimmed_line = trim($line);
// Track block comment state
if (str_contains($trimmed_line, '/*')) {
$in_block_comment = true;
$block_comment_start_line = $line_number;
$block_comment_content = $line;
// Handle single-line block comments
if (str_contains($trimmed_line, '*/')) {
$in_block_comment = false;
$this->check_block_comment_for_legacy($file_path, $block_comment_content, $block_comment_start_line, $lines, $i);
$block_comment_content = '';
}
continue;
}
if ($in_block_comment) {
$block_comment_content .= "\n" . $line;
if (str_contains($trimmed_line, '*/')) {
$in_block_comment = false;
$this->check_block_comment_for_legacy($file_path, $block_comment_content, $block_comment_start_line, $lines, $i);
$block_comment_content = '';
}
continue;
}
}
}
/**
* Check if a block comment contains "legacy" and is followed by a function
*/
private function check_block_comment_for_legacy(string $file_path, string $comment_content, int $comment_start_line, array $lines, int $comment_end_index): void
{
// Check if comment contains "legacy" (case insensitive)
if (!preg_match('/\blegacy\b/i', $comment_content)) {
return;
}
// Look for function definition in the next few lines after comment
for ($j = $comment_end_index + 1; $j < min($comment_end_index + 5, count($lines)); $j++) {
$next_line = trim($lines[$j]);
// Skip empty lines and single-line comments
if (empty($next_line) || str_starts_with($next_line, '//')) {
continue;
}
// Check if this line contains a JavaScript function definition
if (preg_match('/^(static\s+)?function\s+\w+\s*\(/i', $next_line) || // function name()
preg_match('/^\w+\s*:\s*function\s*\(/i', $next_line) || // name: function()
preg_match('/^(static\s+)?\w+\s*\(/i', $next_line) || // static name() or name() (ES6 class method)
preg_match('/^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/i', $next_line) || // const name = function
preg_match('/^(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/i', $next_line) || // const name = () =>
preg_match('/^\w+\s*=\s*(async\s+)?function/i', $next_line) || // name = function
preg_match('/^\w+\s*=\s*(async\s+)?\(/i', $next_line)) { // name = () =>
$this->add_violation(
$file_path,
$comment_start_line,
"🚨 CRITICAL BACKWARDS COMPATIBILITY VIOLATION: JavaScript function with 'legacy' comment detected
RSX Framework Principle: NO BACKWARDS COMPATIBILITY FUNCTIONS ALLOWED
The presence of 'legacy' in function comments indicates backwards compatibility code, which violates the core RSX principle of 'one way to do things'. Legacy functions create:
1. MULTIPLE CODE PATHS for the same functionality
2. MAINTENANCE BURDEN keeping old interfaces working
3. CONFUSION about which method to use
4. TECHNICAL DEBT that accumulates over time
5. DEBUGGING NIGHTMARES when behavior differs between paths
RSX enforces FAIL LOUD architecture - when things change, they should break clearly, not fall back to old implementations.",
trim($comment_content),
"IMMEDIATE ACTION REQUIRED:
1. REMOVE the legacy function entirely
2. UPDATE all callers to use the current implementation
3. DO NOT maintain backwards compatibility
4. If migration is needed, create a one-time migration script that:
- Identifies all usage of the old function
- Updates code to use new function
- Removes old function after migration is complete
For AI/LLM agents: This is a CRITICAL violation. You MUST:
- Search for ALL callers of this legacy function
- Report findings to user before making changes
- Remove the legacy function completely
- Update all code to use the current implementation
- NEVER maintain dual implementations for compatibility
Remember: There is only ONE way to do things in RSX. Legacy functions violate this fundamental principle.",
'critical'
);
return;
}
// If we hit a non-function code line, stop looking
break;
}
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
class JsRspadeInternalMethod_CodeQualityRule extends CodeQualityRule_Abstract
{
protected static $rspade_classes = null;
public function get_id(): string
{
return 'JS-INTERNAL-01';
}
public function get_name(): string
{
return 'JavaScript RSpade Internal Method Usage Check';
}
public function get_description(): string
{
return 'Prohibits calling internal RSpade methods (starting with _) from application code';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Get list of RSpade JavaScript classes from manifest
*/
protected function get_rspade_classes(): array
{
if (static::$rspade_classes === null) {
static::$rspade_classes = [];
// Get all files from manifest
$all_files = Manifest::get_all();
foreach ($all_files as $file_path => $metadata) {
// Check if it's a JavaScript file with a class
$extension = $metadata['extension'] ?? '';
if (in_array($extension, ['*.js']) && isset($metadata['class'])) {
// Handle Windows backslash issue - normalize to forward slashes
$normalized_path = str_replace('\\', '/', $file_path);
// Check if the file is in app/RSpade directory
if (str_contains($normalized_path, 'app/RSpade/')) {
static::$rspade_classes[] = $metadata['class'];
}
}
}
}
return static::$rspade_classes;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get RSpade classes
$rspade_classes = $this->get_rspade_classes();
if (empty($rspade_classes)) {
return; // No RSpade JS classes found
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
// Check for each RSpade class
foreach ($rspade_classes as $class_name) {
// Pattern to match ClassName._method
// Must be preceded by non-alphanumeric or beginning of line
$pattern = '/(?:^|[^a-zA-Z0-9_])(' . preg_quote($class_name, '/') . ')\._[a-zA-Z0-9_]+\s*\(/';
if (preg_match($pattern, $sanitized_line, $matches)) {
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Extract the method call for better error message
preg_match('/' . preg_quote($class_name, '/') . '\._[a-zA-Z0-9_]+/', $sanitized_line, $method_match);
$method_call = $method_match[0] ?? $class_name . '._method';
$this->add_violation(
$file_path,
$line_number,
"Calling internal RSpade method. Methods starting with _ are for framework internal use only.",
trim($original_line),
"Do not call methods starting with underscore on RSpade framework classes. " .
"The method '{$method_call}()' is internal to the framework and may change without notice in updates. " .
"Use only public API methods (those not starting with underscore).",
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* LifecycleMethodsStaticRule - Ensures RSX lifecycle methods are static
*
* This rule checks JavaScript ES6 classes for RSX framework lifecycle methods
* and ensures they are declared as static. These methods are called by the
* framework at specific initialization phases and must be static to work correctly.
*/
class LifecycleMethodsStatic_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* RSX lifecycle methods that must be static
* These are called by the framework during initialization phases
*/
private const LIFECYCLE_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready',
];
public function get_id(): string
{
return 'JS-LIFECYCLE-01';
}
public function get_name(): string
{
return 'RSX Lifecycle Methods Must Be Static';
}
public function get_description(): string
{
return 'Ensures RSX framework lifecycle methods (on_app_ready, etc.) are declared as static in ES6 classes';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* 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.
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* lifecycle methods must be static for the framework's auto-initialization to function correctly.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Check JavaScript file for lifecycle methods that aren't static
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a JavaScript file with a class
if (!isset($metadata['class'])) {
return;
}
// Get class name
$class_name = $metadata['class'];
// Only check classes that extend Jqhtml_Component
if (!\App\RSpade\Core\Manifest\Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component')) {
return;
}
// Check regular (non-static) methods for lifecycle methods that should be static
if (isset($metadata['public_instance_methods']) && is_array($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
if (in_array($method_name, self::LIFECYCLE_METHODS, true)) {
// This lifecycle method is not static - violation!
// Find the line number by searching for the method definition
$line_number = $this->find_method_line($contents, $method_name);
$code_line = $this->extract_code_line($contents, $line_number);
$this->add_violation(
$file_path,
$line_number,
"RSX lifecycle method '{$method_name}' must be declared as static",
$code_line,
$this->get_remediation($method_name, $class_name, $code_line),
'high'
);
}
}
}
}
/**
* Find the line number where a method is defined
*/
private function find_method_line(string $contents, string $method_name): int
{
$lines = explode("\n", $contents);
foreach ($lines as $line_num => $line) {
// Look for method definition (with or without async)
if (preg_match('/\b(async\s+)?' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
return $line_num + 1;
}
}
return 1; // Default to line 1 if not found
}
/**
* Extract code line for a given line number
*/
private function extract_code_line(string $contents, int $line_number): string
{
$lines = explode("\n", $contents);
if (isset($lines[$line_number - 1])) {
return trim($lines[$line_number - 1]);
}
return '';
}
/**
* Get remediation instructions for non-static lifecycle method
*/
private function get_remediation(string $method, ?string $class_name, string $current_line): string
{
$is_async = strpos($current_line, 'async') !== false;
$static_version = $is_async ? "static async {$method}()" : "static {$method}()";
$class_info = $class_name ? " in class {$class_name}" : "";
$phase_description = $this->get_phase_description($method);
return "FRAMEWORK CONVENTION: RSX lifecycle method '{$method}'{$class_info} must be static.
PROBLEM:
The method is currently defined as an instance method, but the RSX framework
calls these methods statically during application initialization.
SOLUTION:
Change the method declaration to: {$static_version}
CURRENT (INCORRECT):
{$current_line}
CORRECTED:
{$static_version} {
// Your initialization code here
}
WHY THIS MATTERS:
- The RSX framework calls lifecycle methods statically on classes
- Instance methods cannot be called without instantiating the class
- The framework does not instantiate classes during initialization
- Using instance methods will cause the initialization to fail silently
LIFECYCLE PHASE:
This method runs during: {$phase_description}
ALL RSX LIFECYCLE METHODS (must be static):
- _on_framework_core_define() - Framework core modules define phase
- _on_framework_modules_define() - Framework extension modules define
- _on_framework_core_init() - Framework core initialization
- on_app_modules_define() - Application modules define phase
- on_app_define() - Application-wide define phase
- _on_framework_modules_init() - Framework modules initialization
- on_app_modules_init() - Application modules initialization
- on_app_init() - Application initialization
- on_app_ready() - Application ready (DOM loaded, components initialized)
Methods prefixed with underscore (_) are internal framework methods.
Application code should typically only use the non-underscore methods.";
}
/**
* Get description of when this lifecycle phase runs
*/
private function get_phase_description(string $method): string
{
$descriptions = [
'_on_framework_core_define' => 'Framework core module definition (internal)',
'_on_framework_modules_define' => 'Framework extension module definition (internal)',
'_on_framework_core_init' => 'Framework core initialization (internal)',
'on_app_modules_define' => 'Application module definition phase',
'on_app_define' => 'Application-wide definition phase',
'_on_framework_modules_init' => 'Framework module initialization (internal)',
'on_app_modules_init' => 'Application module initialization',
'on_app_init' => 'Application initialization',
'on_app_ready' => 'Application ready - DOM loaded, components initialized',
];
return $descriptions[$method] ?? 'Unknown phase';
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class NativeFunction_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-NATIVE-01';
}
public function get_name(): string
{
return 'JavaScript Native Function Usage Check';
}
public function get_description(): string
{
return 'Enforces RSpade functions instead of native JavaScript functions';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Native function patterns and their replacements
$function_patterns = [
// Array.isArray()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])Array\.isArray\s*\(/i',
'native' => 'Array.isArray()',
'replacement' => 'is_array()',
'message' => 'Use is_array() instead of Array.isArray().'
],
// parseFloat()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])parseFloat\s*\(/i',
'native' => 'parseFloat()',
'replacement' => 'float()',
'message' => 'Use float() instead of parseFloat().'
],
// parseInt()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])parseInt\s*\(/i',
'native' => 'parseInt()',
'replacement' => 'int()',
'message' => 'Use int() instead of parseInt().'
],
// String() constructor
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])String\s*\(/i',
'native' => 'String()',
'replacement' => 'str()',
'message' => 'Use str() instead of String().'
],
// encodeURIComponent()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])encodeURIComponent\s*\(/i',
'native' => 'encodeURIComponent()',
'replacement' => 'urlencode()',
'message' => 'Use urlencode() instead of encodeURIComponent().'
],
// decodeURIComponent()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])decodeURIComponent\s*\(/i',
'native' => 'decodeURIComponent()',
'replacement' => 'urldecode()',
'message' => 'Use urldecode() instead of decodeURIComponent().'
],
// JSON.stringify()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])JSON\.stringify\s*\(/i',
'native' => 'JSON.stringify()',
'replacement' => 'json_encode()',
'message' => 'Use json_encode() instead of JSON.stringify().'
],
// JSON.parse()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])JSON\.parse\s*\(/i',
'native' => 'JSON.parse()',
'replacement' => 'json_decode()',
'message' => 'Use json_decode() instead of JSON.parse().'
],
];
foreach ($function_patterns as $check) {
if (preg_match($check['pattern'], $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
$check['message'],
trim($original_line),
"Replace '{$check['native']}' with '{$check['replacement']}'. " .
"RSpade provides PHP-like functions that should be used instead of native JavaScript functions. " .
"This provides consistency across PHP and JavaScript code and ensures predictable behavior.",
'medium'
);
break; // Only report first match per line
}
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class NoControllerSuffix_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-CONTROLLER-01';
}
public function get_name(): string
{
return 'No Controller Suffix in JavaScript';
}
public function get_description(): string
{
return 'JavaScript classes cannot use Controller suffix - reserved for PHP controllers';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check that JavaScript classes don't use Controller suffix
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Check if metadata has a class name (from Manifest)
if (!isset($metadata['class'])) {
return;
}
$class_name = $metadata['class'];
// Check if class name ends with Controller or _controller (case sensitive)
if (str_ends_with($class_name, 'Controller') || str_ends_with($class_name, '_controller')) {
// Find the line where the class is declared
$lines = explode("\n", $contents);
$line_number = 0;
$code_snippet = '';
foreach ($lines as $i => $line) {
// Look for ES6 class declaration
if (preg_match('/\bclass\s+' . preg_quote($class_name) . '\b/', $line)) {
$line_number = $i + 1;
$code_snippet = $this->get_code_snippet($lines, $i);
break;
}
}
$resolution = "JavaScript class '{$class_name}' uses the reserved 'Controller' suffix.\n\n";
$resolution .= "WHY THIS IS PROHIBITED:\n";
$resolution .= "The 'Controller' suffix is reserved exclusively for PHP controller classes because:\n";
$resolution .= "1. It maintains clear separation between frontend and backend code\n";
$resolution .= "2. It prevents confusion when making Ajax_Endpoint_Controller calls from JavaScript\n";
$resolution .= "3. It ensures consistent naming conventions across the codebase\n";
$resolution .= "4. Controllers handle HTTP requests and must be server-side PHP classes\n\n";
$resolution .= "HOW TO FIX:\n";
$resolution .= "Rename the JavaScript class to use a different suffix that describes its purpose:\n";
// Generate suggestions based on common patterns
$base_name = preg_replace('/(Controller|_controller)$/', '', $class_name);
$suggestions = [
$base_name . 'Manager' => 'For managing state or coordinating components',
$base_name . 'Handler' => 'For handling events or user interactions',
$base_name . 'Component' => 'For UI components',
$base_name . 'Service' => 'For service/API interaction logic',
$base_name . 'View' => 'For view-related logic',
$base_name . 'Widget' => 'For reusable UI widgets',
$base_name . 'Helper' => 'For utility/helper functions',
];
$resolution .= "\nSUGGESTED ALTERNATIVES:\n";
foreach ($suggestions as $suggestion => $description) {
$resolution .= "- {$suggestion}: {$description}\n";
}
$resolution .= "\nREMEMBER:\n";
$resolution .= "- JavaScript handles frontend logic and UI interactions\n";
$resolution .= "- PHP Controllers handle HTTP requests and backend logic\n";
$resolution .= "- When JavaScript needs to call a controller, use Ajax.internal() or fetch() to the controller's API endpoint\n\n";
$resolution .= "If this naming is absolutely required (extremely rare), add '@JS-CONTROLLER-01-EXCEPTION' comment.";
$this->add_violation(
$file_path,
$line_number,
"JavaScript class '{$class_name}' uses reserved 'Controller' suffix",
$code_snippet,
$resolution,
'high'
);
}
}
}

View File

@@ -0,0 +1,387 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* NoPageLoadAnimationRule - Detect animations that occur on page load
*
* This rule ensures all page elements appear immediately on initial page load.
* Animations are only allowed in specific scenarios:
* - After loading data via AJAX (discouraged but allowed)
* - In response to user interaction (click, checkbox, etc)
* - For position:absolute overlays like modals
*/
class NoPageLoadAnimation_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Animation methods that we want to detect
* Note: show() and hide() without duration are allowed as they're instant
*/
private const ANIMATION_METHODS = [
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideIn',
'slideOut',
'slideDown',
'slideUp',
'slideToggle'
];
/**
* Init methods where animations are not allowed (unless in anonymous function)
*/
private const INIT_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready'
];
/**
* Event binding methods that indicate user interaction
*/
private const EVENT_METHODS = [
'on',
'click',
'change',
'submit',
'keydown',
'keyup',
'keypress',
'mouseenter',
'mouseleave',
'hover',
'focus',
'blur',
'addEventListener'
];
/**
* AJAX methods that indicate data loading
*/
private const AJAX_METHODS = [
'ajax',
'get',
'post',
'getJSON',
'load',
'done',
'success',
'complete',
'then',
'fetch'
];
/**
* Get the unique identifier for this rule
*/
public function get_id(): string
{
return 'JS-ANIMATION-01';
}
/**
* Get the default severity level
*/
public function get_default_severity(): string
{
return 'critical';
}
/**
* Get the file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.js', '*.jqhtml'];
}
/**
* Get the display name for this rule
*/
public function get_name(): string
{
return 'No Page Load Animation';
}
/**
* Get the description of what this rule checks
*/
public function get_description(): string
{
return 'Detects animations on initial page load - all elements must appear immediately';
}
/**
* Check the file contents for violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip minified files
if (str_contains($file_path, '.min.js')) {
return;
}
$lines = explode("\n", $contents);
$in_init_method = false;
$init_method_name = '';
$brace_depth = 0;
$init_method_depth = 0;
foreach ($lines as $line_num => $line) {
// Check if we're entering an init method
if ($this->_is_entering_init_method($line)) {
$in_init_method = true;
$init_method_name = $this->_extract_method_name($line);
$init_method_depth = $brace_depth;
}
// Track brace depth
$brace_depth += substr_count($line, '{');
$brace_depth -= substr_count($line, '}');
// Check if we're leaving the init method
if ($in_init_method && $brace_depth <= $init_method_depth) {
$in_init_method = false;
$init_method_name = '';
}
// If we're in an init method, check for animations
if ($in_init_method) {
// Check if this line is inside an anonymous function (including arrow functions)
if ($this->_is_in_anonymous_function($lines, $line_num) ||
$this->_is_in_event_handler($lines, $line_num) ||
$this->_is_in_ajax_callback($lines, $line_num)) {
continue; // Allowed context
}
// Check for animation calls
foreach (self::ANIMATION_METHODS as $method) {
// Pattern for jQuery style: .animate( or .fadeIn(
if (preg_match('/\.\s*' . preg_quote($method, '/') . '\s*\(/i', $line)) {
// Check for specific exceptions
if ($this->_is_allowed_animation($line, $lines, $line_num)) {
continue;
}
$this->add_violation(
$line_num + 1,
strpos($line, $method),
"Animation on page load detected: .{$method}()",
trim($line),
"Remove animation from {$init_method_name}(). Elements must appear immediately on page load.\n" .
"If you need to show/hide elements at page load, use .show() or .hide() instead of fade/slide effects.\n" .
"Animations are only allowed:\n" .
"- In response to user interaction (click, change, etc)\n" .
"- After AJAX data loading (discouraged)\n" .
"- For position:absolute overlays (modals)"
);
}
}
// Also check for direct opacity manipulation during init
if (preg_match('/\.css\s*\(\s*[\'"]opacity[\'"]/', $line) &&
(str_contains($line, 'setTimeout') || str_contains($line, 'setInterval'))) {
$this->add_violation(
$line_num + 1,
strpos($line, 'opacity'),
"Delayed opacity change on page load detected",
trim($line),
"Remove opacity animation from {$init_method_name}(). Use CSS for initial styling."
);
}
}
}
}
/**
* Check if we're entering an init method
*/
private function _is_entering_init_method(string $line): bool
{
foreach (self::INIT_METHODS as $method) {
// Match: static method_name() or function method_name()
if (preg_match('/(?:static\s+|function\s+)?' . preg_quote($method, '/') . '\s*\(/i', $line)) {
return true;
}
// Match: method_name: function()
if (preg_match('/' . preg_quote($method, '/') . '\s*:\s*function\s*\(/i', $line)) {
return true;
}
}
return false;
}
/**
* Extract the method name from a line
*/
private function _extract_method_name(string $line): string
{
foreach (self::INIT_METHODS as $method) {
if (str_contains($line, $method)) {
return $method;
}
}
return 'initialization';
}
/**
* Check if current context is inside an anonymous function (including arrow functions)
*/
private function _is_in_anonymous_function(array $lines, int $current_line): bool
{
// Count function depth by looking backwards
$function_depth = 0;
$paren_depth = 0;
$brace_depth = 0;
// Look backwards from current line to find function declarations
for ($i = $current_line; $i >= 0; $i--) {
$line = $lines[$i];
// Count braces to track scope
$brace_depth += substr_count($line, '}');
$brace_depth -= substr_count($line, '{');
// If we've exited all scopes, stop looking
if ($brace_depth > 0) {
break;
}
// Check for anonymous function patterns
// Regular function: function() { or function(args) {
if (preg_match('/function\s*\([^)]*\)\s*{/', $line)) {
return true;
}
// Arrow function: () => { or (args) => {
if (preg_match('/\([^)]*\)\s*=>\s*{/', $line)) {
return true;
}
// Single arg arrow function: arg => {
if (preg_match('/\w+\s*=>\s*{/', $line)) {
return true;
}
// Common callback patterns: setTimeout, setInterval, forEach, map, filter, etc.
if (preg_match('/(setTimeout|setInterval|forEach|map|filter|reduce|some|every|find)\s*\(\s*(function|\([^)]*\)\s*=>|\w+\s*=>)/', $line)) {
return true;
}
// jQuery each pattern: .each(function() or .each((i, el) =>
if (preg_match('/\.each\s*\(\s*(function|\([^)]*\)\s*=>)/', $line)) {
return true;
}
}
return false;
}
/**
* Check if current context is inside an event handler
*/
private function _is_in_event_handler(array $lines, int $current_line): bool
{
// Look backwards for event binding within 10 lines
$start = max(0, $current_line - 10);
for ($i = $current_line; $i >= $start; $i--) {
$line = $lines[$i];
foreach (self::EVENT_METHODS as $event) {
// Check for .on('click', or .click( patterns
if (preg_match('/\.\s*' . preg_quote($event, '/') . '\s*\(/i', $line)) {
return true;
}
// Check for addEventListener
if (str_contains($line, 'addEventListener')) {
return true;
}
}
}
return false;
}
/**
* Check if current context is inside an AJAX callback
*/
private function _is_in_ajax_callback(array $lines, int $current_line): bool
{
// Look backwards for AJAX methods within 10 lines
$start = max(0, $current_line - 10);
for ($i = $current_line; $i >= $start; $i--) {
$line = $lines[$i];
foreach (self::AJAX_METHODS as $ajax) {
if (preg_match('/\.\s*' . preg_quote($ajax, '/') . '\s*\(/i', $line) ||
preg_match('/\$\s*\.\s*' . preg_quote($ajax, '/') . '\s*\(/i', $line)) {
return true;
}
}
// Check for promise patterns
if (str_contains($line, '.then(') || str_contains($line, 'async ') || str_contains($line, 'await ')) {
return true;
}
}
return false;
}
/**
* Check if this is an allowed animation exception
*/
private function _is_allowed_animation(string $line, array $lines, int $line_num): bool
{
// Check for modal or overlay keywords
$allowed_selectors = [
'modal',
'overlay',
'popup',
'dialog',
'tooltip',
'dropdown-menu',
'position-absolute',
'position-fixed'
];
foreach ($allowed_selectors as $selector) {
if (str_contains(strtolower($line), $selector)) {
return true;
}
}
// Check if the element being animated has position:absolute in a nearby style
// This is harder to detect statically, so we'll be conservative
// Check for comments indicating AJAX loading
if ($line_num > 0) {
$prev_line = $lines[$line_num - 1];
if (str_contains(strtolower($prev_line), 'ajax') ||
str_contains(strtolower($prev_line), 'load') ||
str_contains(strtolower($prev_line), 'fetch')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* JavaScript 'this' Usage Rule
*
* PHILOSOPHY: Remove ambiguity about what 'this' refers to in all contexts.
*
* RULES:
* 1. Anonymous functions: Can use 'const $var = $(this)' as first line (jQuery pattern)
* 2. Instance methods: Must use 'const that = this' as first line (constructors exempt)
* 3. Static methods: Use Class_Name OR 'const CurrentClass = this' for polymorphism
* 4. Arrow functions: Ignored (they inherit 'this' context)
* 5. Constructors: Direct 'this' usage allowed for property assignment
*
* PATTERNS:
* - jQuery: const $element = $(this) // Variable must start with $
* - Instance: const that = this // Standard instance aliasing
* - Static (exact): Use Class_Name // When you need exact class
* - Static (polymorphic): const CurrentClass = this // When inherited classes need different behavior
*
* This rule does NOT try to detect all jQuery callbacks - it offers the jQuery
* pattern as an option when 'this' violations are found in anonymous functions.
*/
class ThisUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-THIS-01';
}
public function get_name(): string
{
return "JavaScript 'this' Usage Check";
}
public function get_description(): string
{
return "Enforces clear 'this' patterns: jQuery callbacks use '\$element = \$(this)', instance methods use 'that = this'";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for improper 'this' usage
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check JavaScript files that contain ES6 classes
if (!isset($metadata['class'])) {
return; // Not a class file
}
// Get violations from AST parser
$violations = $this->parse_with_acorn($file_path);
if (empty($violations)) {
return;
}
// Process violations
foreach ($violations as $violation) {
$this->add_violation(
$file_path,
$violation['line'],
$violation['message'],
$violation['codeSnippet'],
$violation['remediation'],
$this->get_default_severity()
);
}
}
/**
* Use Node.js with acorn to parse JavaScript and find violations
* Uses external parser script stored in resources directory
*/
private function parse_with_acorn(string $file_path): array
{
// Setup cache directory
$cache_dir = storage_path('rsx-tmp/cache/code-quality/js-this');
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Cache based on file modification time
$cache_key = md5($file_path) . '-' . filemtime($file_path);
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Check cache first
if (file_exists($cache_file)) {
$cached = json_decode(file_get_contents($cache_file), true);
if ($cached !== null) {
return $cached;
}
}
// Clean old cache files for this source file
$pattern = $cache_dir . '/' . md5($file_path) . '-*.json';
foreach (glob($pattern) as $old_cache) {
if ($old_cache !== $cache_file) {
unlink($old_cache);
}
}
// Path to the parser script
$parser_script = __DIR__ . '/resource/this-usage-parser.js';
if (!file_exists($parser_script)) {
// Parser script missing - fatal error
throw new \RuntimeException("JS-THIS parser script missing: {$parser_script}");
}
// Run Node.js parser with the external script
$output = shell_exec("cd /tmp && node " . escapeshellarg($parser_script) . " " . escapeshellarg($file_path) . " 2>&1");
if (!$output) {
return [];
}
$result = json_decode($output, true);
if (!$result) {
return [];
}
// Check for errors from the parser
if (isset($result['error'])) {
// Parser encountered an error but it's not fatal for the rule
return [];
}
$violations = $result['violations'] ?? [];
// Cache the result
file_put_contents($cache_file, json_encode($violations));
return $violations;
}
}

Some files were not shown because too many files have changed in this diff Show More