Files
rspade_system/app/RSpade/Commands/Refactor/Php/MethodUpdater.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

360 lines
13 KiB
PHP
Executable File

<?php
namespace App\RSpade\Commands\Refactor\Php;
use RuntimeException;
/**
* Updates method references in PHP and Blade files
*/
class MethodUpdater
{
/**
* Rename a method definition in a class file
*
* @param string $file_path Absolute path to class file
* @param string $old_method Old method name
* @param string $new_method New method name
* @return bool True if method was found and renamed
*/
public function rename_method_definition(string $file_path, string $old_method, string $new_method): bool
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
$output = '';
$method_found = false;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for function declarations
if ($token_type === T_FUNCTION) {
// Find the method name (next T_STRING token)
$method_name_index = null;
for ($j = $i + 1; $j < min(count($tokens), $i + 10); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
$method_name_index = $j;
break;
}
}
if ($method_name_index && $tokens[$method_name_index][1] === $old_method) {
// Verify this is a static method 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;
}
}
if ($is_static) {
$method_found = true;
// Output everything up to but not including the method name
$output .= $token_value;
// Skip to method name and replace it
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip past the old method name
$i = $method_name_index;
continue;
}
}
}
$output .= $token_value;
}
if ($method_found) {
$this->write_file_atomically($file_path, $output);
}
return $method_found;
}
/**
* Update static::/self:: references in a class file
*
* @param string $file_path Absolute path to class file
* @param string $old_method Old method name
* @param string $new_method New method name
* @return int Number of replacements made
*/
public function update_static_self_references(string $file_path, string $old_method, string $new_method): int
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
$output = '';
$replacement_count = 0;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for static:: or self::
$is_static_or_self = ($token_type === T_STATIC) ||
($token_type === T_STRING && $token_value === 'self');
if ($is_static_or_self) {
// Check for :: followed by method name
$double_colon_index = null;
$method_name_index = null;
// Find :: - can be either T_DOUBLE_COLON token or string '::'
for ($j = $i + 1; $j < min(count($tokens), $i + 5); $j++) {
$is_double_colon_token = false;
if ($tokens[$j] === '::') {
$is_double_colon_token = true;
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_DOUBLE_COLON) {
$is_double_colon_token = true;
}
if ($is_double_colon_token) {
$double_colon_index = $j;
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
if ($double_colon_index) {
// Find method name after ::
for ($j = $double_colon_index + 1; $j < min(count($tokens), $double_colon_index + 5); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
if ($tokens[$j][1] === $old_method) {
$method_name_index = $j;
}
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
}
if ($method_name_index) {
// Output static/self
$output .= $token_value;
// Output everything between static/self and method name (whitespace, ::)
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip to after method name
$i = $method_name_index;
$replacement_count++;
continue;
}
}
$output .= $token_value;
}
if ($replacement_count > 0) {
$this->write_file_atomically($file_path, $output);
}
return $replacement_count;
}
/**
* Update Class::method references across all files
*
* @param array $references Map of file paths to occurrences from MethodReferenceScanner
* @param string $class_name Class name
* @param string $class_fqcn Fully qualified class name
* @param string $old_method Old method name
* @param string $new_method New method name
* @return int Number of files updated
*/
public function update_method_references(array $references, string $class_name, string $class_fqcn, string $old_method, string $new_method): int
{
$updated_count = 0;
foreach ($references as $file_path => $occurrences) {
if ($this->update_file_method_references($file_path, $class_name, $class_fqcn, $old_method, $new_method)) {
$updated_count++;
}
}
return $updated_count;
}
/**
* Update method references in a single file
*/
protected function update_file_method_references(string $file_path, string $class_name, string $class_fqcn, string $old_method, string $new_method): bool
{
$content = file_get_contents($file_path);
// Determine if this is a Blade file
if (str_ends_with($file_path, '.blade.php')) {
$updated_content = $this->replace_method_in_blade($content, $class_name, $class_fqcn, $old_method, $new_method);
} else {
$updated_content = $this->replace_method_in_php($content, $class_name, $class_fqcn, $old_method, $new_method);
}
// Check if any changes were made
if ($updated_content === $content) {
return false;
}
$this->write_file_atomically($file_path, $updated_content);
return true;
}
/**
* Replace method references in PHP content
*/
protected function replace_method_in_php(string $content, string $class_name, string $class_fqcn, string $old_method, string $new_method): string
{
$tokens = token_get_all($content);
$output = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Look for simple class name (T_STRING) or FQCN (T_NAME_FULLY_QUALIFIED) followed by :: and method name
$is_match = false;
if ($token_type === T_STRING && $token_value === $class_name) {
$is_match = true;
} elseif (defined('T_NAME_FULLY_QUALIFIED') && $token_type === T_NAME_FULLY_QUALIFIED) {
// Strip leading backslash for comparison
$normalized_token = ltrim($token_value, '\\');
$normalized_fqcn = ltrim($class_fqcn, '\\');
if ($normalized_token === $normalized_fqcn) {
$is_match = true;
}
}
if ($is_match) {
$double_colon_index = null;
$method_name_index = null;
// Find :: - can be either T_DOUBLE_COLON token or string '::'
for ($j = $i + 1; $j < min(count($tokens), $i + 5); $j++) {
$is_double_colon_token = false;
if ($tokens[$j] === '::') {
$is_double_colon_token = true;
} elseif (is_array($tokens[$j]) && $tokens[$j][0] === T_DOUBLE_COLON) {
$is_double_colon_token = true;
}
if ($is_double_colon_token) {
$double_colon_index = $j;
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
if ($double_colon_index) {
// Find method name after ::
for ($j = $double_colon_index + 1; $j < min(count($tokens), $double_colon_index + 5); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
if ($tokens[$j][1] === $old_method) {
$method_name_index = $j;
}
break;
}
if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
break;
}
}
}
if ($method_name_index) {
// Output class name (simple or FQCN)
$output .= $token_value;
// Output everything between class and method name (whitespace, ::)
for ($j = $i + 1; $j < $method_name_index; $j++) {
$output .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
// Output new method name
$output .= $new_method;
// Skip to after method name
$i = $method_name_index;
continue;
}
}
$output .= $token_value;
}
return $output;
}
/**
* Replace method references in Blade content
*/
protected function replace_method_in_blade(string $content, string $class_name, string $class_fqcn, string $old_method, string $new_method): string
{
// Pattern to match simple Class::method with word boundaries
$pattern1 = '/\b' . preg_quote($class_name, '/') . '\s*::\s*' . preg_quote($old_method, '/') . '\b/';
$content = preg_replace($pattern1, $class_name . '::' . $new_method, $content);
// Pattern to match FQCN \Namespace\Class::method
$escaped_fqcn = preg_quote($class_fqcn, '/');
$pattern2 = '/\\\\?' . $escaped_fqcn . '\s*::\s*' . preg_quote($old_method, '/') . '\b/';
$content = preg_replace($pattern2, '\\' . $class_fqcn . '::' . $new_method, $content);
return $content;
}
/**
* Write file atomically using temp file
*/
protected function write_file_atomically(string $file_path, string $content): void
{
$temp_file = $file_path . '.refactor-temp';
if (file_put_contents($temp_file, $content) === false) {
throw new RuntimeException("Failed to write temp file: {$temp_file}");
}
if (!rename($temp_file, $file_path)) {
@unlink($temp_file);
throw new RuntimeException("Failed to replace file: {$file_path}");
}
}
}