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>
347 lines
14 KiB
PHP
Executable File
347 lines
14 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Commands\Refactor;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use App\RSpade\Core\Manifest\Manifest;
|
|
use App\RSpade\Commands\Refactor\Php\MethodReferenceScanner;
|
|
use App\RSpade\Commands\Refactor\Php\MethodUpdater;
|
|
use App\RSpade\Upstream\RefactorLog;
|
|
use RuntimeException;
|
|
|
|
class RenamePhpClassFunction_Command extends Command
|
|
{
|
|
protected $signature = 'rsx:refactor:rename_php_class_function
|
|
{class_name : The class containing the method}
|
|
{old_method : Current static method name}
|
|
{new_method : New static method name}
|
|
{--force-rename-static-self : Only rename self::/static:: references in class}
|
|
{--skip-subclasses : Don\'t recursively rename in subclasses}
|
|
{--dry-run : Show what would be changed without modifying files}';
|
|
|
|
protected $description = 'Rename a static method across all RSX files including subclasses';
|
|
|
|
public function handle()
|
|
{
|
|
$class_name = $this->argument('class_name');
|
|
$old_method = $this->argument('old_method');
|
|
$new_method = $this->argument('new_method');
|
|
$force_rename_static_self = $this->option('force-rename-static-self');
|
|
$skip_subclasses = $this->option('skip-subclasses');
|
|
$dry_run = $this->option('dry-run');
|
|
|
|
// Initialize manifest
|
|
Manifest::init();
|
|
|
|
// Phase 1: Validation
|
|
$this->info("Validating method: {$class_name}::{$old_method}");
|
|
|
|
try {
|
|
$class_path = Manifest::php_find_class($class_name);
|
|
$class_metadata = Manifest::get_file($class_path);
|
|
} catch (RuntimeException $e) {
|
|
$this->error("Class '{$class_name}' not found in manifest");
|
|
return 1;
|
|
}
|
|
|
|
$absolute_class_path = base_path($class_path);
|
|
|
|
// Build FQCN from metadata
|
|
$class_fqcn = $class_metadata['namespace'] . '\\' . $class_metadata['class'];
|
|
|
|
// Check if method exists and is static
|
|
$method_info = $this->find_method_in_class($absolute_class_path, $old_method);
|
|
|
|
if ($method_info === null) {
|
|
if ($force_rename_static_self) {
|
|
$this->line("<fg=yellow>⚠</> Method '{$old_method}' not found in class (allowed with --force-rename-static-self)");
|
|
$method_was_renamed = false;
|
|
} else {
|
|
$this->error("Method '{$class_name}::{$old_method}' not found in class definition");
|
|
return 1;
|
|
}
|
|
} else {
|
|
if (!$method_info['is_static']) {
|
|
$this->error("Method '{$class_name}::{$old_method}' is not static. Only static methods can be renamed.");
|
|
return 1;
|
|
}
|
|
|
|
$this->info("<fg=green>✓</> Method found and is static");
|
|
|
|
// Check if destination method already exists
|
|
$new_method_info = $this->find_method_in_class($absolute_class_path, $new_method);
|
|
if ($new_method_info !== null) {
|
|
$this->error("Method '{$class_name}::{$new_method}' already exists. Cannot rename.");
|
|
return 1;
|
|
}
|
|
|
|
$this->info("<fg=green>✓</> Destination method available");
|
|
$method_was_renamed = true;
|
|
}
|
|
|
|
// Phase 1.5: Pre-flight check for framework file modifications
|
|
$this->preflight_check_framework_files($class_name, $class_fqcn, $old_method, $absolute_class_path, $method_was_renamed, $skip_subclasses);
|
|
|
|
if ($dry_run) {
|
|
$this->info("");
|
|
$this->info("<fg=yellow>DRY RUN - No files will be modified</>");
|
|
return $this->preview_changes($class_name, $class_fqcn, $old_method, $new_method, $absolute_class_path, $method_was_renamed, $skip_subclasses);
|
|
}
|
|
|
|
// Phase 2: Rename method definition (if it exists)
|
|
if ($method_was_renamed) {
|
|
$this->info("");
|
|
$this->info("Renaming method definition...");
|
|
|
|
$updater = new MethodUpdater();
|
|
$renamed = $updater->rename_method_definition($absolute_class_path, $old_method, $new_method);
|
|
|
|
if ($renamed) {
|
|
$this->info("<fg=green>✓</> Updated: " . $class_path);
|
|
} else {
|
|
$this->error("Failed to rename method definition");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Phase 3a: ALWAYS update static::/self:: references in class file
|
|
$this->info("");
|
|
$this->info("Updating static::/self:: references in class...");
|
|
|
|
$updater = $updater ?? new MethodUpdater();
|
|
$static_self_count = $updater->update_static_self_references($absolute_class_path, $old_method, $new_method);
|
|
|
|
if ($static_self_count > 0) {
|
|
$this->info("<fg=green>✓</> Updated {$static_self_count} static::/self:: reference(s) in class");
|
|
} else {
|
|
$this->line("<fg=gray>No static::/self:: references found in class</>");
|
|
}
|
|
|
|
// Phase 3b: Update Class::method references in all files (only if method was renamed)
|
|
if ($method_was_renamed) {
|
|
$this->info("");
|
|
$this->info("Scanning for references to '{$class_name}::{$old_method}'...");
|
|
|
|
$scanner = new MethodReferenceScanner();
|
|
$references = $scanner->find_all_method_references($class_name, $class_fqcn, $old_method);
|
|
|
|
if (!empty($references)) {
|
|
$this->info("Found " . count($references) . " file(s) with references:");
|
|
$this->info("");
|
|
|
|
foreach ($references as $file_path => $occurrences) {
|
|
$relative_path = str_replace(base_path() . '/', '', $file_path);
|
|
$this->line(" <fg=cyan>{$relative_path}</>");
|
|
$this->line(" <fg=gray>" . count($occurrences) . " occurrence(s)</>");
|
|
}
|
|
|
|
$this->info("");
|
|
$this->info("Updating references...");
|
|
|
|
$updated_count = $updater->update_method_references($references, $class_name, $class_fqcn, $old_method, $new_method);
|
|
$this->info("<fg=green>✓</> Updated {$updated_count} file(s)");
|
|
} else {
|
|
$this->line("<fg=gray>No external references found</>");
|
|
}
|
|
}
|
|
|
|
// Phase 4: Recursive subclass renaming (unless --skip-subclasses)
|
|
if (!$skip_subclasses) {
|
|
$this->info("");
|
|
$this->info("Processing subclasses...");
|
|
|
|
$subclasses = Manifest::php_get_subclasses_of($class_name, true);
|
|
|
|
if (!empty($subclasses)) {
|
|
$this->info("Found " . count($subclasses) . " subclass(es): " . implode(', ', $subclasses));
|
|
|
|
foreach ($subclasses as $subclass) {
|
|
$exit_code = Artisan::call('rsx:refactor:rename_php_class_function', [
|
|
'class_name' => $subclass,
|
|
'old_method' => $old_method,
|
|
'new_method' => $new_method,
|
|
'--force-rename-static-self' => true,
|
|
'--skip-subclasses' => true,
|
|
]);
|
|
|
|
if ($exit_code === 0) {
|
|
$this->line(" <fg=green>✓</> Updated {$subclass}");
|
|
} else {
|
|
$this->line(" <fg=yellow>⚠</> Skipped {$subclass} (no references)");
|
|
}
|
|
}
|
|
} else {
|
|
$this->line("<fg=gray>No subclasses found</>");
|
|
}
|
|
}
|
|
|
|
// Log refactor operation for upstream tracking
|
|
RefactorLog::log_refactor(
|
|
"rsx:refactor:rename_php_class_function {$class_name} {$old_method} {$new_method}",
|
|
$class_path
|
|
);
|
|
|
|
// Success summary
|
|
$this->info("");
|
|
$this->info("<fg=green>✓</> Successfully renamed {$class_name}::{$old_method} → {$class_name}::{$new_method}");
|
|
$this->info("");
|
|
$this->info("Next steps:");
|
|
$this->info(" 1. Review changes: git diff");
|
|
$this->info(" 2. Test application");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Find a method in a class file and return metadata
|
|
*
|
|
* @return array|null ['is_static' => bool, 'line' => int] or null if not found
|
|
*/
|
|
protected function find_method_in_class(string $file_path, string $method_name): ?array
|
|
{
|
|
$content = file_get_contents($file_path);
|
|
$tokens = token_get_all($content);
|
|
|
|
for ($i = 0; $i < count($tokens); $i++) {
|
|
if (!is_array($tokens[$i])) {
|
|
continue;
|
|
}
|
|
|
|
if ($tokens[$i][0] === T_FUNCTION) {
|
|
// Find method name
|
|
$method_found = false;
|
|
for ($j = $i + 1; $j < min(count($tokens), $i + 10); $j++) {
|
|
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING && $tokens[$j][1] === $method_name) {
|
|
$method_found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($method_found) {
|
|
// Check if static by looking backwards
|
|
$is_static = false;
|
|
for ($k = $i - 1; $k >= max(0, $i - 20); $k--) {
|
|
if (!is_array($tokens[$k])) continue;
|
|
if ($tokens[$k][0] === T_STATIC) {
|
|
$is_static = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'is_static' => $is_static,
|
|
'line' => $tokens[$i][2]
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Pre-flight check: Ensure no framework files would be modified when not in framework developer mode
|
|
*/
|
|
protected function preflight_check_framework_files(string $class_name, string $class_fqcn, string $old_method, string $class_path, bool $method_was_renamed, bool $skip_subclasses): void
|
|
{
|
|
// Skip check if in framework developer mode
|
|
if (config('rsx.code_quality.is_framework_developer', false)) {
|
|
return;
|
|
}
|
|
|
|
$framework_files = [];
|
|
|
|
// Check if class file itself is a framework file
|
|
$relative_class_path = str_replace(base_path() . '/', '', $class_path);
|
|
if (str_starts_with($relative_class_path, 'app/RSpade/')) {
|
|
$framework_files[] = $relative_class_path;
|
|
}
|
|
|
|
// Check for method references in other files (if method exists)
|
|
if ($method_was_renamed) {
|
|
$scanner = new MethodReferenceScanner();
|
|
$references = $scanner->find_all_method_references($class_name, $class_fqcn, $old_method);
|
|
|
|
foreach ($references as $file_path => $occurrences) {
|
|
$relative_path = str_replace(base_path() . '/', '', $file_path);
|
|
if (str_starts_with($relative_path, 'app/RSpade/')) {
|
|
$framework_files[] = $relative_path;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check subclasses (if not skipped)
|
|
if (!$skip_subclasses) {
|
|
$subclasses = Manifest::php_get_subclasses_of($class_name, true);
|
|
foreach ($subclasses as $subclass) {
|
|
try {
|
|
$subclass_path = Manifest::php_find_class($subclass);
|
|
if (str_starts_with($subclass_path, 'app/RSpade/')) {
|
|
$framework_files[] = $subclass_path;
|
|
}
|
|
} catch (RuntimeException $e) {
|
|
// Subclass not found in manifest - skip
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deduplicate
|
|
$framework_files = array_unique($framework_files);
|
|
|
|
// Throw fatal error if framework files would be modified
|
|
if (!empty($framework_files)) {
|
|
$this->error("");
|
|
$this->error("FATAL: This refactor would modify " . count($framework_files) . " framework file(s) in app/RSpade/");
|
|
$this->error("");
|
|
$this->error("Framework files that would be modified:");
|
|
foreach ($framework_files as $file) {
|
|
$this->error(" - {$file}");
|
|
}
|
|
$this->error("");
|
|
$this->error("Refactoring core framework files is only available to RSpade framework maintainers.");
|
|
$this->error("Set IS_FRAMEWORK_DEVELOPER=true in .env if you are maintaining the framework.");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preview changes without modifying files
|
|
*/
|
|
protected function preview_changes(string $class_name, string $class_fqcn, string $old_method, string $new_method, string $class_path, bool $method_was_renamed, bool $skip_subclasses): int
|
|
{
|
|
$this->info("");
|
|
|
|
if ($method_was_renamed) {
|
|
$this->line("Would rename method definition in:");
|
|
$this->line(" " . str_replace(base_path() . '/', '', $class_path));
|
|
$this->info("");
|
|
|
|
$scanner = new MethodReferenceScanner();
|
|
$references = $scanner->find_all_method_references($class_name, $class_fqcn, $old_method);
|
|
|
|
if (!empty($references)) {
|
|
$this->line("Would update " . count($references) . " file(s) with {$class_name}::{$old_method} references");
|
|
}
|
|
}
|
|
|
|
$static_self_refs = (new MethodReferenceScanner())->find_static_self_references($class_path, $old_method);
|
|
if (!empty($static_self_refs)) {
|
|
$this->line("Would update " . count($static_self_refs) . " static::/self:: reference(s) in class");
|
|
}
|
|
|
|
if (!$skip_subclasses) {
|
|
$subclasses = Manifest::php_get_subclasses_of($class_name, true);
|
|
if (!empty($subclasses)) {
|
|
$this->info("");
|
|
$this->line("Would recursively update " . count($subclasses) . " subclass(es):");
|
|
foreach ($subclasses as $subclass) {
|
|
$this->line(" - {$subclass}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|