= 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}"); } } }