Files
rspade_system/app/RSpade/Commands/Refactor/Php/FileUpdater.php
root f6ac36c632 Enhance refactor commands with controller-aware Route() updates and fix code quality violations
Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 06:21:56 +00:00

430 lines
15 KiB
PHP
Executable File

<?php
namespace App\RSpade\Commands\Refactor\Php;
use RuntimeException;
/**
* Updates class references in PHP and Blade files with atomic writes
*/
class FileUpdater
{
/**
* Update class references in all affected files
*
* @param array $references Map of file paths to occurrences
* @param string $old_class Old class name
* @param string $new_class New class name
* @param string $source_fqcn Source class FQCN from manifest
* @return int Number of files updated
*/
public function update_class_references(array $references, string $old_class, string $new_class, string $source_fqcn): int
{
$updated_count = 0;
foreach ($references as $file_path => $occurrences) {
if ($this->update_file($file_path, $old_class, $new_class, $source_fqcn)) {
$updated_count++;
}
}
return $updated_count;
}
/**
* Update a single file with class name replacements
*
* @param string $file_path Absolute path to file
* @param string $old_class Old class name
* @param string $new_class New class name
* @param string $source_fqcn Source class FQCN from manifest
* @return bool True if file was updated
*/
protected function update_file(string $file_path, string $old_class, string $new_class, string $source_fqcn): bool
{
$content = file_get_contents($file_path);
if ($content === false) {
throw new RuntimeException("Failed to read file: {$file_path}");
}
// Replace class name with context awareness
$updated_content = $this->replace_class_name($content, $old_class, $new_class, $source_fqcn);
// Check if any changes were made
if ($updated_content === $content) {
return false;
}
// Write atomically using temp file
$temp_file = $file_path . '.refactor-temp';
if (file_put_contents($temp_file, $updated_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}");
}
return true;
}
/**
* Replace class name in content with context awareness
*
* Uses token-based replacement to avoid false positives in strings/comments
*/
protected function replace_class_name(string $content, string $old_class, string $new_class, string $source_fqcn): string
{
// For Blade files, we need special handling
if (str_contains($content, '@') || str_contains($content, '{{')) {
return $this->replace_in_blade($content, $old_class, $new_class, $source_fqcn);
}
// For pure PHP files, use token-based replacement
return $this->replace_in_php($content, $old_class, $new_class, $source_fqcn);
}
/**
* Replace class name in PHP content using token analysis
*/
protected function replace_in_php(string $content, string $old_class, string $new_class, string $source_fqcn): string
{
$tokens = token_get_all($content);
$output = '';
// Calculate new FQCN for string literal replacement
$source_fqcn_normalized = ltrim($source_fqcn, '\\');
$new_fqcn = preg_replace('/' . preg_quote($old_class, '/') . '$/', $new_class, $source_fqcn_normalized);
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
// String tokens are passed through as-is
if (!is_array($token)) {
$output .= $token;
continue;
}
$token_type = $token[0];
$token_value = $token[1];
// Replace FQCN in string literals
if ($token_type === T_CONSTANT_ENCAPSED_STRING) {
$quote = $token_value[0];
$string_content = substr($token_value, 1, -1);
// Check if string exactly equals source FQCN (with or without leading \)
if ($string_content === $source_fqcn_normalized || $string_content === '\\' . $source_fqcn_normalized) {
$leading_slash = str_starts_with($string_content, '\\') ? '\\' : '';
$output .= $quote . $leading_slash . $new_fqcn . $quote;
continue;
}
}
// Check if this is start of a FQCN
if ($token_type === T_STRING || $token_type === T_NS_SEPARATOR) {
$fqcn_result = $this->check_and_replace_fqcn($tokens, $i, $old_class, $new_class);
if ($fqcn_result !== null) {
// We found and replaced a FQCN, output the replacement and skip ahead
$output .= $fqcn_result['replacement'];
$i = $fqcn_result['end_index'];
continue;
}
}
// Replace simple T_STRING tokens that match the old class name
if ($token_type === T_STRING && $token_value === $old_class) {
// Verify this is actually a class reference (not in string/comment)
if ($this->is_class_reference_context($tokens, $i)) {
$output .= $new_class;
} else {
$output .= $token_value;
}
} else {
$output .= $token_value;
}
}
return $output;
}
/**
* Check if we're at the start of a FQCN and if it should be replaced
*
* @return array|null Returns ['replacement' => string, 'end_index' => int] or null
*/
protected function check_and_replace_fqcn(array $tokens, int $start_index, string $old_class, string $new_class): ?array
{
// Build the full FQCN from consecutive T_STRING and T_NS_SEPARATOR tokens
$fqcn_parts = [];
$i = $start_index;
while ($i < count($tokens)) {
if (!is_array($tokens[$i])) {
break;
}
$token_type = $tokens[$i][0];
if ($token_type === T_STRING) {
$fqcn_parts[] = $tokens[$i][1];
$i++;
} elseif ($token_type === T_NS_SEPARATOR) {
$fqcn_parts[] = '\\';
$i++;
} elseif ($token_type === T_WHITESPACE) {
// Skip whitespace
$i++;
} else {
break;
}
}
// Need at least 2 parts for a FQCN (namespace + class)
if (count($fqcn_parts) < 3) {
return null;
}
$fqcn = implode('', $fqcn_parts);
// Extract class name (last part after final \)
$class_name = basename(str_replace('\\', '/', $fqcn));
// Check if class name matches
if ($class_name !== $old_class) {
return null;
}
// Check if FQCN starts with Rsx\ or App\RSpade\
$normalized_fqcn = ltrim($fqcn, '\\');
if (!str_starts_with($normalized_fqcn, 'Rsx\\') && !str_starts_with($normalized_fqcn, 'App\\RSpade\\')) {
return null;
}
// Build replacement FQCN with new class name
$namespace = dirname(str_replace('\\', '/', $fqcn));
$namespace = str_replace('/', '\\', $namespace);
if ($namespace === '.') {
$replacement = $new_class;
} else {
// Preserve leading \ if original had it
$leading_slash = str_starts_with($fqcn, '\\') ? '\\' : '';
$replacement = $leading_slash . $namespace . '\\' . $new_class;
}
return [
'replacement' => $replacement,
'end_index' => $i - 1
];
}
/**
* Check if a token is in a valid class reference context
*/
protected function is_class_reference_context(array $tokens, int $index): bool
{
// Look backwards for context clues
for ($i = $index - 1; $i >= max(0, $index - 10); $i--) {
if (!is_array($tokens[$i])) {
// Skip non-token characters like ( ) , etc
continue;
}
$prev_token = $tokens[$i][0];
// Valid contexts
if (in_array($prev_token, [T_CLASS, T_NEW, T_EXTENDS, T_IMPLEMENTS, T_INSTANCEOF, T_USE, T_DOUBLE_COLON])) {
return true;
}
// Skip whitespace, namespace separators, and comments
if (in_array($prev_token, [T_WHITESPACE, T_NS_SEPARATOR, T_COMMENT, T_DOC_COMMENT])) {
continue;
}
// For type hints, check if we're after ( or ,
if ($prev_token === T_STRING) {
// Could be part of a namespace
continue;
}
// If we hit something else meaningful, stop looking
if (!in_array($prev_token, [T_WHITESPACE, T_NS_SEPARATOR, T_STRING])) {
break;
}
}
// Look forwards for static access
for ($i = $index + 1; $i < min(count($tokens), $index + 5); $i++) {
if (!is_array($tokens[$i])) {
continue;
}
$next_token = $tokens[$i][0];
if ($next_token === T_DOUBLE_COLON) {
return true;
}
if (in_array($next_token, [T_WHITESPACE, T_NS_SEPARATOR])) {
continue;
}
break;
}
// Check if this appears to be a type hint (before a variable)
for ($i = $index + 1; $i < min(count($tokens), $index + 5); $i++) {
if (!is_array($tokens[$i])) {
continue;
}
$next_token = $tokens[$i][0];
// Type hint if followed by a variable
if ($next_token === T_VARIABLE) {
return true;
}
if ($next_token === T_WHITESPACE) {
continue;
}
break;
}
return false;
}
/**
* Replace class name in Blade content
*
* Uses simple regex replacement since Blade mixes PHP and HTML
*/
protected function replace_in_blade(string $content, string $old_class, string $new_class, string $source_fqcn): string
{
// Pattern matches class name as a word boundary (not part of another identifier)
// This prevents replacing "User" in "UserController" or in the middle of strings
$pattern = '/\b' . preg_quote($old_class, '/') . '\b/';
// Replace with word boundary check to avoid partial matches
$updated = preg_replace_callback($pattern, function ($matches) use ($old_class, $new_class, $content) {
// Additional safety: check if this looks like it's in a string literal
// This is a simple heuristic - if surrounded by quotes, skip it
$pos = strpos($content, $matches[0]);
if ($pos !== false) {
// Check 50 chars before and after for quote context
$before = substr($content, max(0, $pos - 50), 50);
$after = substr($content, $pos, 50);
// Count quotes before and after
$quotes_before = substr_count($before, '"') + substr_count($before, "'");
$quotes_after = substr_count($after, '"') + substr_count($after, "'");
// If odd number of quotes before/after, likely inside a string
if ($quotes_before % 2 === 1 || $quotes_after % 2 === 1) {
return $matches[0]; // Keep original
}
}
return $new_class;
}, $content);
return $updated;
}
/**
* Update controller references in Route() calls and Ajax endpoints
* Only processes files in rsx/ directory that are in the manifest
*
* @param string $old_class Old controller class name
* @param string $new_class New controller class name
* @return int Number of files updated
*/
public function update_controller_route_references(string $old_class, string $new_class): int
{
$updated_count = 0;
// Get all files from manifest in rsx/ directory
$manifest = \App\RSpade\Core\Manifest\Manifest::get_all();
foreach ($manifest as $relative_path => $metadata) {
// Only process files in rsx/ directory
if (!str_starts_with($relative_path, 'rsx/')) {
continue;
}
$extension = $metadata['extension'] ?? '';
$file_path = base_path($relative_path);
if (!file_exists($file_path)) {
continue;
}
$content = file_get_contents($file_path);
$updated_content = $content;
// Apply replacements based on file type
if ($extension === 'js' || $extension === 'jqhtml') {
// Replace Rsx.Route('OLD_CLASS' and Rsx.Route("OLD_CLASS"
$updated_content = preg_replace(
'/\bRsx\.Route\([\'"]' . preg_quote($old_class, '/') . '[\'"]/',
'Rsx.Route(\'' . $new_class . '\'',
$updated_content
);
}
if ($extension === 'jqhtml' || $extension === 'blade.php') {
// Replace $controller="OLD_CLASS"
$updated_content = preg_replace(
'/(\s\$controller=["\'])' . preg_quote($old_class, '/') . '(["\'])/',
'$1' . $new_class . '$2',
$updated_content
);
}
if ($extension === 'jqhtml') {
// Replace unquoted attribute assignments: $attr=OLD_CLASS followed by space, dot, or >
// Pattern: (attribute name)=(controller name)(space|dot|>)
$updated_content = preg_replace(
'/(\$[\w_]+)=' . preg_quote($old_class, '/') . '\b(?=[\s.>])/',
'$1=' . $new_class,
$updated_content
);
}
if ($extension === 'js') {
// Replace Ajax endpoint calls: OLD_CLASS.method(
// Pattern: (whitespace|;|(|[)OLD_CLASS.
$updated_content = preg_replace(
'/(?<=[\s;(\[])' . preg_quote($old_class, '/') . '\b(?=\.)/',
$new_class,
$updated_content
);
}
if ($extension === 'php' || $extension === 'blade.php') {
// Replace Rsx::Route('OLD_CLASS' and Rsx::Route("OLD_CLASS"
$updated_content = preg_replace(
'/\bRsx::Route\([\'"]' . preg_quote($old_class, '/') . '[\'"]/',
'Rsx::Route(\'' . $new_class . '\'',
$updated_content
);
}
// Write if changed
if ($updated_content !== $content) {
file_put_contents($file_path, $updated_content);
$updated_count++;
}
}
return $updated_count;
}
}