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

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

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

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;
}
}