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>
796 lines
35 KiB
PHP
Executable File
796 lines
35 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\RSpade\Commands\Rsx;
|
||
|
||
use Illuminate\Console\Command;
|
||
use App\RSpade\CodeQuality\CodeQualityChecker;
|
||
use App\RSpade\Core\Manifest\Manifest;
|
||
use Symfony\Component\Finder\Finder;
|
||
|
||
class Rsx_Check_Command extends Command
|
||
{
|
||
/**
|
||
* The name and signature of the console command.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $signature = 'rsx:check
|
||
{paths?* : Files or directories to check (defaults to rsx/ and app/)}
|
||
{--fix : Attempt to automatically fix violations (not implemented yet)}
|
||
{--json : Output results as JSON}
|
||
{--silent : Only show output if violations are found}
|
||
{--pre-commit-tests : Enable pre-commit checks including rsx/temp validation}
|
||
{--convention : Show convention violations}
|
||
{--max= : Maximum number of violations to display}';
|
||
|
||
/**
|
||
* The console command description.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $description = 'Check code for RSpade framework standards compliance';
|
||
|
||
/**
|
||
* Execute the console command.
|
||
*
|
||
* @return int
|
||
*/
|
||
public function handle()
|
||
{
|
||
// Place .placeholder files in all empty directories
|
||
$this->place_placeholder_files();
|
||
|
||
// Clean up any existing code quality remediation report
|
||
$remediation_file = base_path('CODE_QUALITY_REMEDIATION.md');
|
||
if (file_exists($remediation_file)) {
|
||
unlink($remediation_file);
|
||
}
|
||
|
||
$quiet = $this->option('silent');
|
||
|
||
if (!$quiet) {
|
||
$this->info('🔍 RSX Code Quality Checker');
|
||
$this->info('==============================');
|
||
$this->newLine();
|
||
}
|
||
|
||
// Get pre-commit-tests flag
|
||
$pre_commit_tests = $this->option('pre-commit-tests');
|
||
|
||
// Get paths from argument or use defaults
|
||
$paths = $this->argument('paths');
|
||
$using_default_paths = empty($paths);
|
||
if ($using_default_paths) {
|
||
$paths = ['rsx', 'app'];
|
||
}
|
||
|
||
// Tell the checker to exclude manifest-time rules
|
||
// They've already run when the manifest was loaded, so running them again is redundant
|
||
$config['exclude_manifest_time_rules'] = true;
|
||
|
||
// Initialize code quality checker with config
|
||
$config = [
|
||
'pre_commit_tests' => $pre_commit_tests,
|
||
'using_default_paths' => $using_default_paths
|
||
];
|
||
CodeQualityChecker::init($config);
|
||
|
||
$total_files = 0;
|
||
$controller_files = 0;
|
||
|
||
// Collect all files to check
|
||
$files_to_check = [];
|
||
|
||
// Process each path (file or directory)
|
||
foreach ($paths as $path) {
|
||
// Handle absolute or relative paths
|
||
if (str_starts_with($path, '/')) {
|
||
$full_path = $path;
|
||
} else {
|
||
$full_path = base_path($path);
|
||
}
|
||
|
||
// Check if path exists
|
||
if (!file_exists($full_path)) {
|
||
$this->error("❌ Error: Path does not exist: {$path}");
|
||
return 1;
|
||
}
|
||
|
||
// Check if it's a file
|
||
if (is_file($full_path)) {
|
||
// Get scan directories from config
|
||
$scan_directories = config('rsx.manifest.scan_directories', []);
|
||
$relative_path = str_replace(base_path() . '/', '', $full_path);
|
||
|
||
// Check if file is in allowed directories
|
||
$in_valid_path = false;
|
||
|
||
// Special case: Allow Console Command files
|
||
if (str_starts_with($relative_path, 'app/Console/Commands/')) {
|
||
$in_valid_path = true;
|
||
} else {
|
||
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) {
|
||
$in_valid_path = true;
|
||
break;
|
||
}
|
||
} 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, '/')) {
|
||
$in_valid_path = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$in_valid_path) {
|
||
// Build list of allowed directories (excluding specific files)
|
||
$allowed_dirs = [];
|
||
foreach ($scan_directories as $scan_path) {
|
||
if (!str_contains($scan_path, '.')) {
|
||
$allowed_dirs[] = $scan_path;
|
||
}
|
||
}
|
||
|
||
$this->error("❌ Error: File '{$path}' is not in an allowed directory");
|
||
$this->warn("⚠️ Allowed directories:");
|
||
foreach ($allowed_dirs as $dir) {
|
||
$this->info(" - {$dir}/");
|
||
}
|
||
$this->newLine();
|
||
$this->warn("📝 For testing code quality rules, place your test file in rsx/temp/");
|
||
$this->info(" Example: mv {$path} " . base_path('rsx/temp/') . basename($path));
|
||
|
||
// Add framework developer message if enabled
|
||
if (config('rsx.code_quality.is_framework_developer', false)) {
|
||
$this->newLine();
|
||
$this->comment("💡 Framework Developer Mode: If testing a rule that only targets app/RSpade/,");
|
||
$this->comment(" place the file in app/RSpade/temp/ for testing (create the directory if needed).");
|
||
$this->info(" Example: mkdir -p " . base_path('app/RSpade/temp') . " && mv {$path} " . base_path('app/RSpade/temp/') . basename($path));
|
||
}
|
||
|
||
$this->newLine();
|
||
return 1;
|
||
}
|
||
|
||
// Check if file is in the manifest (skip for Console Commands)
|
||
if (!str_starts_with($relative_path, 'app/Console/Commands/')) {
|
||
$manifest = Manifest::get_all();
|
||
$found_in_manifest = false;
|
||
foreach ($manifest as $manifest_path => $metadata) {
|
||
if (realpath($full_path) === realpath(base_path($manifest_path))) {
|
||
$found_in_manifest = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$found_in_manifest) {
|
||
$this->error("❌ Error: File '{$path}' is not in the manifest");
|
||
$this->warn("⚠️ The file must be indexed by the manifest to be checked");
|
||
$this->info(" Try running: php artisan rsx:manifest:build");
|
||
$this->newLine();
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
if (!$quiet) {
|
||
$this->info("Checking file: {$path}");
|
||
}
|
||
$total_files++;
|
||
$files_to_check[] = $full_path;
|
||
|
||
// Count controller/model files for stats
|
||
if ($this->is_controller_file($full_path) || $this->is_model_file($full_path)) {
|
||
$controller_files++;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Check if it's a directory
|
||
if (!is_dir($full_path)) {
|
||
$this->error("❌ Error: Path is neither a file nor a directory: {$path}");
|
||
return 1;
|
||
}
|
||
|
||
// Get scan directories from config for validation
|
||
$scan_directories = config('rsx.manifest.scan_directories', []);
|
||
$relative_dir = str_replace(base_path() . '/', '', $full_path);
|
||
$relative_dir = rtrim($relative_dir, '/');
|
||
|
||
// Check if directory is in allowed paths
|
||
$in_valid_path = false;
|
||
foreach ($scan_directories as $scan_path) {
|
||
// Skip specific file entries
|
||
if (str_contains($scan_path, '.')) {
|
||
continue;
|
||
}
|
||
|
||
$scan_dir = rtrim($scan_path, '/');
|
||
if ($relative_dir === $scan_dir ||
|
||
str_starts_with($relative_dir, $scan_dir . '/') ||
|
||
str_starts_with($scan_dir, $relative_dir . '/')) {
|
||
$in_valid_path = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Special handling for 'app' directory to scan app/RSpade subdirectories
|
||
if ($relative_dir === 'app') {
|
||
$in_valid_path = true;
|
||
}
|
||
|
||
if (!$in_valid_path) {
|
||
// Build list of allowed directories (excluding specific files)
|
||
$allowed_dirs = [];
|
||
foreach ($scan_directories as $scan_path) {
|
||
if (!str_contains($scan_path, '.')) {
|
||
$allowed_dirs[] = $scan_path;
|
||
}
|
||
}
|
||
|
||
$this->error("❌ Error: Directory '{$path}' is not within allowed scan directories");
|
||
$this->warn("⚠️ Allowed directories:");
|
||
foreach ($allowed_dirs as $dir) {
|
||
$this->info(" - {$dir}/");
|
||
}
|
||
$this->newLine();
|
||
return 1;
|
||
}
|
||
|
||
if (!$quiet) {
|
||
$this->info("Scanning: {$path}/");
|
||
}
|
||
$is_rsx_dir = ($path === 'rsx');
|
||
|
||
// Collect PHP files
|
||
$php_finder = new Finder();
|
||
$php_finder->files()
|
||
->in($full_path)
|
||
->name('*.php')
|
||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache']);
|
||
|
||
foreach ($php_finder as $file) {
|
||
$total_files++;
|
||
$files_to_check[] = $file->getPathname();
|
||
|
||
// Count controller/model files for stats
|
||
if ($is_rsx_dir) {
|
||
if ($this->is_controller_file($file->getPathname()) || $this->is_model_file($file->getPathname())) {
|
||
$controller_files++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// === Additional File Collection ===
|
||
// Collect all files for filename case checks (RSX only)
|
||
if ($is_rsx_dir) {
|
||
$all_files_finder = new Finder();
|
||
$all_files_finder->files()
|
||
->in($full_path)
|
||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache', 'resource']);
|
||
|
||
foreach ($all_files_finder as $file) {
|
||
$file_path = $file->getPathname();
|
||
if (!in_array($file_path, $files_to_check)) {
|
||
$files_to_check[] = $file_path;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Collect JavaScript files for all directories
|
||
$js_finder = new Finder();
|
||
$js_finder->files()
|
||
->in($full_path)
|
||
->name('*.js')
|
||
->exclude(['vendor', 'node_modules', 'storage', 'public', 'bootstrap/cache']);
|
||
|
||
foreach ($js_finder as $file) {
|
||
$file_path = $file->getPathname();
|
||
if (!in_array($file_path, $files_to_check)) {
|
||
$files_to_check[] = $file_path;
|
||
$total_files++;
|
||
}
|
||
}
|
||
|
||
// Collect JSON files for all directories
|
||
$json_finder = new Finder();
|
||
$json_finder->files()
|
||
->in($full_path)
|
||
->name('*.json')
|
||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache']);
|
||
|
||
foreach ($json_finder as $file) {
|
||
$file_path = $file->getPathname();
|
||
if (!in_array($file_path, $files_to_check)) {
|
||
$files_to_check[] = $file_path;
|
||
$total_files++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// If using default paths, also include Console Commands for rules that support them
|
||
if ($using_default_paths) {
|
||
$commands_dir = base_path('app/Console/Commands');
|
||
if (is_dir($commands_dir)) {
|
||
if (!$quiet) {
|
||
$this->info("Including Console Commands for supporting rules...");
|
||
}
|
||
|
||
$command_files = [];
|
||
$command_finder = new Finder();
|
||
$command_finder->files()
|
||
->in($commands_dir)
|
||
->name('*.php')
|
||
->exclude(['vendor', 'node_modules']);
|
||
|
||
foreach ($command_finder as $file) {
|
||
$command_files[] = $file->getPathname();
|
||
}
|
||
|
||
// Add Console Command files to the check list
|
||
// The checker will determine which rules support them
|
||
foreach ($command_files as $file) {
|
||
if (!in_array($file, $files_to_check)) {
|
||
$files_to_check[] = $file;
|
||
$total_files++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Filter out app/RSpade files if not in framework developer mode
|
||
if (!config('rsx.code_quality.is_framework_developer', false)) {
|
||
$files_to_check = array_filter($files_to_check, function($file) {
|
||
$relative_path = str_replace(base_path() . '/', '', $file);
|
||
return !str_starts_with($relative_path, 'app/RSpade/');
|
||
});
|
||
// Re-index array after filtering
|
||
$files_to_check = array_values($files_to_check);
|
||
}
|
||
|
||
// Run checks through the new modular system
|
||
CodeQualityChecker::check_files($files_to_check);
|
||
|
||
// Get violations
|
||
$violations = CodeQualityChecker::get_violations();
|
||
$convention_count = CodeQualityChecker::get_collector()->get_convention_count();
|
||
$show_conventions = $this->option('convention');
|
||
|
||
// If showing conventions, get convention violations
|
||
if ($show_conventions) {
|
||
$violations = CodeQualityChecker::get_collector()->get_convention_violations_as_arrays();
|
||
}
|
||
|
||
// In quiet mode, only output if there are violations
|
||
if ($quiet && empty($violations) && !$convention_count) {
|
||
return 0;
|
||
}
|
||
|
||
// If violations exist or not in quiet mode, show output
|
||
if (!empty($violations) || !$quiet) {
|
||
// Always show header and counts when there are violations (even in quiet mode)
|
||
if (!empty($violations) && $quiet) {
|
||
$this->info('🔍 RSX Code Quality Checker');
|
||
$this->info('==============================');
|
||
$this->newLine();
|
||
}
|
||
|
||
if (!$quiet) {
|
||
$this->newLine();
|
||
}
|
||
$this->info("Files scanned: {$total_files}");
|
||
$this->info("Controllers/Models checked: {$controller_files}");
|
||
$this->newLine();
|
||
}
|
||
|
||
// Show convention warning if not displaying conventions
|
||
if (!$show_conventions && $convention_count > 0) {
|
||
$this->warn("⚠️ Warning: There are ({$convention_count}) RSX application convention warnings, call rsx:check --convention to view the warnings");
|
||
$this->newLine();
|
||
}
|
||
|
||
if (empty($violations)) {
|
||
if ($show_conventions) {
|
||
$this->info('✅ No convention violations found!');
|
||
} else {
|
||
$this->info('✅ No code standard violations found!');
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// Apply --max limit if specified
|
||
$total_violations = count($violations);
|
||
$max_violations = $this->option('max');
|
||
$violations_truncated = false;
|
||
|
||
if ($max_violations && is_numeric($max_violations) && $max_violations > 0) {
|
||
$max_violations = (int)$max_violations;
|
||
if ($total_violations > $max_violations) {
|
||
$violations = array_slice($violations, 0, $max_violations);
|
||
$violations_truncated = true;
|
||
}
|
||
}
|
||
|
||
// Output violations
|
||
if ($this->option('json')) {
|
||
$this->output_json($violations, $total_violations, $violations_truncated);
|
||
} else {
|
||
$this->output_table($violations, $total_violations, $violations_truncated);
|
||
}
|
||
|
||
// Summary
|
||
$this->newLine();
|
||
if ($show_conventions) {
|
||
$this->warn('⚠️ Found ' . $total_violations . ' convention violation(s)');
|
||
} else {
|
||
$this->error('❌ Found ' . $total_violations . ' code standard violation(s)');
|
||
}
|
||
|
||
// Show truncation message if violations were limited
|
||
if ($violations_truncated) {
|
||
$shown = count($violations);
|
||
$remaining = $total_violations - $shown;
|
||
$this->newLine();
|
||
$this->comment("📄 Showing first {$shown} violations. {$remaining} additional violations are present.");
|
||
} elseif ($max_violations && $total_violations > 0 && $total_violations <= $max_violations) {
|
||
$this->newLine();
|
||
$this->comment("📄 All {$total_violations} violations shown (within --max={$max_violations} limit).");
|
||
}
|
||
|
||
// Notice about potential validator errors
|
||
$this->newLine();
|
||
$this->comment('ℹ️ Notice: While these violations have been detected, please verify each issue carefully.');
|
||
$this->comment(' The validators themselves may contain errors. If a violation appears incorrect,');
|
||
$this->comment(' thoroughly review both the code and the validator logic. It may be necessary');
|
||
$this->comment(' to fix the validator rather than the code.');
|
||
|
||
// Convention violations don't cause non-zero exit
|
||
if ($show_conventions) {
|
||
return 0;
|
||
}
|
||
|
||
// Always return non-zero exit code when violations are found
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* Output violations as a table
|
||
*/
|
||
protected function output_table($violations, $total_violations = null, $violations_truncated = false)
|
||
{
|
||
$this->warn('Code Standard Violations:');
|
||
$this->newLine();
|
||
|
||
$current_file = null;
|
||
|
||
foreach ($violations as $violation) {
|
||
// Debug output to understand violations
|
||
if (!isset($violation['file'])) {
|
||
$this->error("Invalid violation structure: " . json_encode($violation));
|
||
continue;
|
||
}
|
||
|
||
// Group by file for better readability
|
||
$relative_file = str_replace(base_path() . '/', '', $violation['file']);
|
||
|
||
if ($current_file !== $relative_file) {
|
||
if ($current_file !== null) {
|
||
$this->newLine();
|
||
}
|
||
$this->line("<fg=cyan>{$relative_file}</>");
|
||
$current_file = $relative_file;
|
||
}
|
||
|
||
$line = str_pad($violation['line'], 6, ' ', STR_PAD_LEFT);
|
||
|
||
// Use simplified output format for all violations
|
||
$type = $violation['type'] ?? 'unknown';
|
||
$message = $violation['message'] ?? 'No message provided';
|
||
$code = $violation['code'] ?? null;
|
||
$resolution = $violation['resolution'] ?? null;
|
||
|
||
// Show the violation
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=cyan>[{$type}]</>");
|
||
$this->line(" <fg=gray> ↳</> {$message}");
|
||
|
||
// Show code snippet if available
|
||
if ($code) {
|
||
$this->line(" <fg=gray> Code:</> <fg=red>{$code}</>");
|
||
}
|
||
|
||
// Show resolution if available
|
||
if ($resolution) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$resolution}</>");
|
||
}
|
||
|
||
continue; // Skip the old if-elseif chain
|
||
|
||
// OLD CODE BELOW - TO BE REMOVED
|
||
if (false && $violation['type'] === 'filename_case') {
|
||
$filename = $violation['filename'] ?? 'unknown';
|
||
$suggested = $violation['suggested'] ?? '';
|
||
$this->line(" <fg=yellow>File:</> <fg=red>{$filename}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'defensive_coding') {
|
||
$variable = $violation['variable'] ?? 'unknown';
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'mass_assignment_property') {
|
||
$property = $violation['property'] ?? 'unknown';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>protected \${$property}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'instance_method') {
|
||
$method = $violation['method'] ?? 'unknown';
|
||
$class = $violation['class'] ?? 'unknown';
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'instance_pattern') {
|
||
$class = $violation['class'] ?? 'unknown';
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'this_usage') {
|
||
$class = $violation['class'] ?? 'unknown';
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'this_assignment') {
|
||
$class = $violation['class'] ?? 'unknown';
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'var_keyword') {
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'jquery_usage') {
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'document_ready') {
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'fallback_legacy') {
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'function_exists') {
|
||
$code = $violation['code'] ?? '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'php_syntax_error') {
|
||
$this->line(" <fg=red>⚠ PHP Syntax Error</>");
|
||
if ($line > 0) {
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$violation['message']}</>");
|
||
} else {
|
||
$this->line(" <fg=red>{$violation['message']}</>");
|
||
}
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'js_syntax_error') {
|
||
$this->line(" <fg=red>⚠ JavaScript Syntax Error</>");
|
||
if ($line > 0) {
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$violation['message']}</>");
|
||
} else {
|
||
$this->line(" <fg=red>{$violation['message']}</>");
|
||
}
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'json_syntax_error') {
|
||
$this->line(" <fg=red>⚠ JSON Syntax Error</>");
|
||
$this->line(" <fg=red>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'enhanced_filename') {
|
||
// Enhanced filename violations
|
||
$filename = $violation['filename'] ?? 'N/A';
|
||
$this->line(" <fg=red>⚠ Filename: {$filename}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=red>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
// Split resolution into lines for better readability
|
||
$resolution_lines = explode("\n", $violation['resolution']);
|
||
foreach ($resolution_lines as $res_line) {
|
||
if (trim($res_line)) {
|
||
$this->line(" <fg=gray> </> <fg=green>{$res_line}</>");
|
||
}
|
||
}
|
||
}
|
||
} elseif ($violation['type'] === 'unauthorized_root_file') {
|
||
// Unauthorized files in project root
|
||
$filename = $violation['filename'] ?? 'N/A';
|
||
$this->line(" <fg=red>⚠ Unauthorized file: {$filename}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'test_file_in_rsx_root') {
|
||
// Test files in rsx/ root directory
|
||
$filename = $violation['filename'] ?? 'N/A';
|
||
$this->line(" <fg=red>⚠ Test file in rsx/: {$filename}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'test_file_in_rsx_temp') {
|
||
// Test files in rsx/temp directory
|
||
$filename = $violation['filename'] ?? 'N/A';
|
||
$this->line(" <fg=red>⚠ File in rsx/temp/: {$filename}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=cyan>{$violation['message']}</>");
|
||
if (isset($violation['resolution'])) {
|
||
$this->line(" <fg=gray> ↳ Fix:</> <fg=green>{$violation['resolution']}</>");
|
||
}
|
||
} elseif ($violation['type'] === 'db_table_usage') {
|
||
// DB::table() usage violations
|
||
$code = $violation['code'] ?? '';
|
||
$table = isset($violation['table']) ? " (table: {$violation['table']})" : '';
|
||
$this->line(" <fg=yellow>Line {$line}:</> <fg=red>{$code}</>");
|
||
$this->line(" <fg=gray> ↳</> <fg=red>{$violation['message']}{$table}</>");
|
||
if (isset($violation['resolution'])) {
|
||
// Split resolution into lines for better readability
|
||
$resolution_lines = explode("\n", $violation['resolution']);
|
||
foreach ($resolution_lines as $res_line) {
|
||
if (trim($res_line)) {
|
||
$this->line(" <fg=gray> </> <fg=green>{$res_line}</>");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Output violations as JSON
|
||
*/
|
||
protected function output_json($violations, $total_violations = null, $violations_truncated = false)
|
||
{
|
||
$output = [
|
||
'violations' => $violations,
|
||
'summary' => [
|
||
'total_violations' => $total_violations ?: count($violations),
|
||
'shown_violations' => count($violations),
|
||
'violations_truncated' => $violations_truncated,
|
||
'files_affected' => count(array_unique(array_column($violations, 'file')))
|
||
]
|
||
];
|
||
|
||
$this->line(json_encode($output, JSON_PRETTY_PRINT));
|
||
}
|
||
|
||
/**
|
||
* Check if file is a controller
|
||
*/
|
||
protected function is_controller_file($file_path)
|
||
{
|
||
// Check if filename matches controller pattern
|
||
return preg_match('/_controller\.php$/', basename($file_path));
|
||
}
|
||
|
||
/**
|
||
* Check if file is a model
|
||
*/
|
||
protected function is_model_file($file_path)
|
||
{
|
||
// Check if file is in Models directory or ends with Model.php
|
||
if (strpos($file_path, '/Models/') !== false || strpos($file_path, '/models/') !== false) {
|
||
return true;
|
||
}
|
||
|
||
if (preg_match('/Model\.php$/', $file_path)) {
|
||
return true;
|
||
}
|
||
|
||
// Quick check for extending Model
|
||
$content = file_get_contents($file_path);
|
||
return preg_match('/extends\s+.*Model/', $content);
|
||
}
|
||
|
||
/**
|
||
* Place .placeholder files in all empty directories
|
||
* Remove .placeholder files from non-empty directories
|
||
*/
|
||
protected function place_placeholder_files()
|
||
{
|
||
$is_framework_developer = config('rsx.code_quality.is_framework_developer', false);
|
||
|
||
// Only manage .placeholder files in framework developer mode
|
||
// Application developers don't need them
|
||
if (!$is_framework_developer) {
|
||
return;
|
||
}
|
||
|
||
$base_path = base_path();
|
||
|
||
// Initialize variables for \exec_safe() call
|
||
$output = [];
|
||
$return_code = 0;
|
||
|
||
// Find all empty directories and place .placeholder files (exclude vendor and node_modules)
|
||
\exec_safe("find \"$base_path\" -type d -empty -not -path '*/vendor/*' -not -path '*/node_modules/*' -exec touch {}/.placeholder \\; 2>/dev/null", $output, $return_code);
|
||
|
||
// Find and remove zero-length .placeholder files from non-empty directories
|
||
$directory_iterator = new \RecursiveDirectoryIterator($base_path, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||
$filter_iterator = new \RecursiveCallbackFilterIterator($directory_iterator, function ($current) {
|
||
// Skip vendor and node_modules directories
|
||
if ($current->isDir()) {
|
||
$dirname = $current->getFilename();
|
||
if ($dirname === 'vendor' || $dirname === 'node_modules') {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
|
||
$iterator = new \RecursiveIteratorIterator(
|
||
$filter_iterator,
|
||
\RecursiveIteratorIterator::SELF_FIRST
|
||
);
|
||
|
||
foreach ($iterator as $file) {
|
||
if ($file->isFile() && $file->getFilename() === '.placeholder') {
|
||
$placeholder_path = $file->getPathname();
|
||
|
||
// Check if file is zero length
|
||
if ($file->getSize() === 0) {
|
||
$dir = dirname($placeholder_path);
|
||
|
||
// Count other files in the directory (excluding .placeholder)
|
||
$files = array_diff(scandir($dir), ['.', '..', '.placeholder']);
|
||
|
||
// If directory has other files, remove .placeholder
|
||
if (count($files) > 0) {
|
||
unlink($placeholder_path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Silently succeed - this is a maintenance operation
|
||
}
|
||
}
|