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>
360 lines
13 KiB
PHP
Executable File
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}");
|
|
}
|
|
}
|
|
}
|